package Bio::Graphics::Browser2::Render;

use strict;	
use warnings;

use JSON;
use Digest::MD5 'md5_hex';
use CGI qw(:standard param request_method header url iframe img span div br center url_param);
use Carp qw(croak cluck);
use File::Basename 'dirname','basename';
use Text::Tabs;
use Data::Dumper;
use English;

use Bio::Graphics::Browser2::I18n;
use Bio::Graphics::Browser2::PluginSet;
use Bio::Graphics::Browser2::Shellwords;
use Bio::Graphics::Browser2::Action;
use Bio::Graphics::Browser2::Region;
use Bio::Graphics::Browser2::RegionSearch;
use Bio::Graphics::Browser2::RenderPanels;
use Bio::Graphics::Browser2::RemoteSet;
use Bio::Graphics::Browser2::SubtrackTable;
use Bio::Graphics::Browser2::TrackDumper;
use Bio::Graphics::Browser2::Util qw[modperl_request url_label];
use Bio::Graphics::Browser2::UserTracks;
use Bio::Graphics::Browser2::UserDB;
use Bio::Graphics::Browser2::Session;
use Bio::Graphics::Browser2::Render::SnapshotManager;
use POSIX ":sys_wait_h";

use constant VERSION              => 2.0;
use constant DEBUG                => 0;
use constant TRACE_RUN            => 0;
use constant TRACE                => 0; # shows top level events
use constant OVERVIEW_SCALE_LABEL => 'Overview Scale';
use constant REGION_SCALE_LABEL   => 'Region Scale';
use constant DETAIL_SCALE_LABEL   => 'Detail Scale';
use constant EMPTY_IMAGE_HEIGHT   => 12;
use constant MAX_SEGMENT          => 1_000_000;
use constant TOO_MANY_SEGMENTS    => 5_000;
use constant OVERVIEW_RATIO       => 1.0;
use constant GROUP_SEPARATOR      => "\x1d";
use constant LABEL_SEPARATOR      => "\x1e";

my %PLUGINS;         # cache initialized plugins
my $FCGI_REQUEST;    # stash fastCGI request handle
my $FCGI_RUNNING=0;  # true if fastCGI is running
my $STATE;           # stash state for use by callbacks

# new() can be called with two arguments: ($data_source,$session)
# or with one argument: ($globals)
# in the latter case, it will invoke this code:
#   $session = $globals->authorized_session()
#   $globals->update_data_source($session)
#   $source = $globals->create_data_source($session->source)

sub new {
  my $class = shift;

  my ($data_source, $session, $requested_id, $authority);

  if (@_ == 2) {
    ($data_source, $session) = @_;
  } elsif (@_ == 1) {
    my $globals = shift;
    $requested_id = param('id')        || CGI::cookie('gbrowse_sess');
    $authority    = param('authority') || CGI::cookie('authority');
    my $shared_ok = Bio::Graphics::Browser2::Action->shared_lock_ok(scalar param('action'));
    $session      = $globals->authorized_session($requested_id, 
						 $authority,
						 $shared_ok);
    $globals->update_data_source($session);
    $data_source = $globals->create_data_source($session->source);
  } else {
    croak "usage: ".__PACKAGE__."->new(\$globals) or ->new(\$data_source,\$session)";
  }

  my $self = bless {},ref $class || $class;

  $self->data_source($data_source);
  $self->session($session);
  $self->state($session->page_settings);
  $self->set_language();
  $self->set_signal_handlers();
  $self;
}

sub set_signal_handlers {
    my $self = shift;
    $SIG{CHLD} = sub {
	while ((my $kid = waitpid(-1,WNOHANG))>0) {
	    1;
	}
    };
}

sub data_source {
    my $self = shift;
    my $d = $self->{data_source};
    $self->{data_source} = shift if @_;
    $d;
}

sub session {
    my $self = shift;
    my $d = $self->{session};
    $self->{session} = shift if @_;
    warn "d= $d" if DEBUG;
    warn "self->session= $self->{session}" if DEBUG;
    $d;
}

sub state {
  my $self = shift;
  my $d = $self->{state};
  $STATE = $self->{state}= shift if @_;
  $d;
}

# this is a STATIC method that can be used by callbacks as
# Bio::Graphics::Browser2::Render->request->{name}
sub request {
    return $STATE;
}

sub error_message {
    my $self = shift;
    my $d = $self->{error_message};
    $self->{error_message} = shift if @_;
    $d;
}

sub is_admin {
    my $self = shift;
    my $login = $self->session->username      or return;
    my $admin = $self->globals->admin_account or return;
    return $login eq $admin;
}

sub userdb {
    my $self = shift;
    return $self->{userdb} if exists $self->{userdb};
    unless ($self->globals->user_accounts) {
	$self->{userdb} = undef;
	return;
    }
    my $userdb = $self->{userdb} ||= Bio::Graphics::Browser2::UserDB->new($self->globals);
    return $userdb;
}

# User Tracks - Returns a list of a user's tracks.
sub user_tracks {
    my $self  = shift;

    # note: Bio::Graphics::Browser2::AdminTracks is a subclass of UserTracks
    # that is defined within the UserTracks.pm file.
    my $class = $self->is_admin ? 'Bio::Graphics::Browser2::AdminTracks'
                                : 'Bio::Graphics::Browser2::UserTracks';
    
    $self->{usertracks} ||= $class->new($self->data_source,$self->session);
    return $self->{usertracks};
}

sub get_usertrack_labels {
    my $self = shift;
    return $self->{'.user_labels'} if exists $self->{'.user_labels'};
    my $userdata = $self->user_tracks;
    my @files    = $userdata->tracks;  # misnomer -- should be "uploads" or "files"
    for my $f (@files) {
	my @labels = $userdata->labels($f);
	$self->{'.user_labels'}{$_}=$f foreach @labels;
    }
    return $self->{'.user_labels'} ||= {};
}

sub remote_sources {
  my $self = shift;
  my $d = $self->{remote_sources};
  $self->{remote_sources} = shift if @_;
  $d;
}

sub db {
  my $self = shift;
  my $d = $self->{db};
  $self->{db} = shift if @_;
  $d;
}

sub plugins {
  my $self = shift;
  my $d = $self->{plugins};
  $self->{plugins} = shift if @_;
  $d;
}

sub plugin_name {
    my $self = shift;
    my $label = shift;
    #my ($id) = $label =~ /^plugin:(\w+)/;
    my (undef,$id) = split(/:/,$label);
    return $self->plugins->plugin($id)->name;
}

sub debug {
    my $self = shift;
    return $self->{debug} if exists $self->{debug};
    return $self->{debug} = DEBUG || $self->data_source->global_setting('debug');
}

sub DESTROY {
    my $self = shift;
    warn "[$$] $self: destroy she said" if DEBUG;
}

sub destroy {
    my $self = shift;
    # because the login manager maintains a copy of the render object,
    # we need to explicitly destroy it to avoid memory leaks.
    if (my $lm = $self->{login_manager}) {
	$lm->destroy;
	delete $self->{login_manager};
    }
    $self->session->unlock if $self->session;
}

###################################################################################
#
# RUN CODE HERE
#
###################################################################################
sub run {
  my $self = shift;
  my $fh   = shift || \*STDOUT;
  my $old_fh = select($fh);

  my $debug = $self->debug || TRACE;

  warn "[$$] RUN(): ",
       request_method(),': ',
       url(-path=>1),' ',
       query_string() if $debug || TRACE_RUN;
  warn "[$$] session id = ",$self->session->id if $debug;

  $ENV{FTP_PASSIVE}=1;  # so that later calls to LWP will work on ftp:// urls

  $self->set_source() && return;

  my $session = $self->session;
  my $source  = $self->data_source;
  if ($session->private) {
      $source->set_username($session->username);
  } else {
      $source->set_username(undef);
  }

  ##warn "username = ",$session->username;

  if ($source->must_authenticate) {
      if ($session->private && 
	  $self->user_authorized_for_source($session->username))
      {
	  # login session - make sure that the data source has the information needed
	  # to restrict tracks according to policy
      } else {
	  # authentication required, but not a login session, so initiate authentication request
	  $self->force_authentication;
	  $session->flush;
	  $session->unlock;
	  return;
      }
  }

  warn "[$$] add_user_tracks()" if $debug;
  $self->add_user_tracks($self->data_source);

  warn "[$$] testing for asynchronous event()" if $debug;
  if ($self->run_asynchronous_event) {
      warn "[$$] asynchronous exit" if $debug;
      $self->session->unlock;
      return ;
  }
  
  if (my $file = param('share_link')) {
      $self->share_link($file);
  }

  warn "[$$] init()"         if $debug;
  $self->init();

  warn "[$$] update_state()" if $debug;
  $self->update_state();
  
  # EXPERIMENTAL CODE -- GET RID OF THE URL PARAMETERS
  if ($ENV{QUERY_STRING} && $ENV{QUERY_STRING} =~ /reset/) {
      print CGI::redirect(CGI::url(-absolute=>1,-path_info=>1));
  } else {
      warn "[$$] render()" if $debug;
      $self->render();
  }

  warn "[$$] cleanup" if $debug;
  $self->cleanup();
  select($old_fh);

  warn "[$$] session flush" if $debug;
  $self->session->flush;
  $self->session->unlock;
  
  delete $self->{usertracks};
  warn "[$$] synchronous exit" if $debug;
}

sub user_authorized_for_source {
    my $self = shift;
    my $username = shift;
    my $source   = $self->data_source;
    my $globals  = $source->globals;
    my $session  = $self->session;
    my $plugins = $self->init_plugins();
    $source->set_username($username);
    return $globals->authorized($source->name,$username,$plugins->auth_plugin)
	&& $source->authorized('general');
}

sub set_source {
    my $self = shift;

    my $source = $self->session->source;

    if (CGI::unescape(CGI::path_info()) ne CGI::unescape("/$source/")) {
	my $args = CGI::query_string();
	my $url  = CGI::url(-absolute=>1,-path_info=>0);
	$url     =~ s!(gbrowse[^/]*)(?\!.*gbrowse)/.+$!$1!;  # fix CGI/Apache bug
	$url    .= "/$source/";
	$url .= "?$args" if $args && $args !~ /^source=/;
	print CGI::redirect($url);
	return 1;
    }
    return;
}

sub init {
    my $self = shift;
    warn "init()" if DEBUG;
    warn "init_database()" if DEBUG;
    $self->init_database();
    warn "init_plugins()" if DEBUG;
    $self->init_plugins();
    warn "init_remote_sources()" if DEBUG;
    $self->init_remote_sources();
    warn "set_default_state()" if DEBUG;
    $self->set_default_state();
    warn "init done" if DEBUG;
}

# this prints out the HTTP data from an asynchronous event
sub run_asynchronous_event {
    my $self = shift;
    my ($status, $mime_type, $data, %headers) = $self->asynchronous_event or return;

    warn "[$$] asynchronous event returning status=$status, mime-type=$mime_type" if TRACE_RUN;

    # add the cookies!
    $headers{-cookie} = [$self->state_cookie,$self->auth_cookie];

    if ($status == 204) { # no content
		print CGI::header( -status => '204 No Content', %headers );
    } elsif ($status == 302) { # redirect
	print CGI::redirect($data);
    } elsif ($mime_type eq 'application/json') {
		print CGI::header(
			-status			=> $status,
			-cache_control	=> 'no-cache',
			-charset		=> $self->translate('CHARSET'),
			-type			=> $mime_type,
			%headers),
			JSON::to_json($data);
    } else {
		print CGI::header(
			-status        => $status,
			-cache_control => 'no-cache',
			-charset       => $self->translate('CHARSET'),
			-type          => $mime_type,
			%headers),
			$data;
    }
    return 1;  # no further processing needed
}

# handle asynchronous events
#
# Asynchronous requests. See Bio::Graphics::Browser2::Action for dispatch
# table. Each request returns a three element list of format:
#    ($http_status,$mime_type,$body_data)
sub asynchronous_event {
    my $self     = shift;
    my $settings = $self->state;
    my $events;

    warn "[$$] asynchronous event(",query_string(),")" if TRACE_RUN;

    # TO ADD AN ASYNCHRONOUS REQUEST...
    # 1. Give the request a unique name, such as "foo"
    # 2. Arrange for the client to POST or GET the document URL with a CGI
    #    argument list that includes "action=foo".
    # 3. Modify Bio::Graphics::Browser2::Action to include a method named
    #    ACTION_foo that processes the request. The method will receive 
    #    the CGI object as its argument, and is expected to return
    #    a three-item list consisting of the HTTP status code, the MIME type,
    #    and the message contents in the appropriate format for the MIME type.
    #    Within the method call $self->render()
    #    to get this Bio::Graphics::Browser2::Render object.

    # legacy URLs
    my $dispatch = Bio::Graphics::Browser2::Action->new($self);
    if (my @result = $dispatch->handle_legacy_calls($CGI::Q,$self)) {
		return @result;
    }

    if ( my $track_name = param('display_citation') ) {
         my $html = $self->display_citation($track_name);
         return ( 200, 'text/html', $html );
    }

    elsif (my $action = param('action')) {
		my $method   = "ACTION_${action}";
		unless ($dispatch->can($method)) {
			return (401,'text/plain',"invalid action: '$action'");
		}
		return $dispatch->$method($CGI::Q);
    }

    else {
		return;
    }
}

sub authorize_user {
    my $self = shift;
    my ($username,$id,$remember,$using_openid) = @_;
    my ($session,$error);

    my $userdb     = $self->userdb;
    return unless $userdb->username_from_sessionid($id) eq $username;
    
    warn "Checking current session" if DEBUG;
    my $current = $self->session->id;
    if ($current eq $id) {
        warn "Using current session" if DEBUG;
        $session = $self->session;
    } elsif ($self->session->private) { # trying to login without logging out?
	$session = $self->globals->session($id);  # create/retrieve session
    } else {
        warn "Retrieving old session" if DEBUG;
	$session = $self->globals->session($id);  # create/retrieve session
    }
    unless ($session->id eq $id) {
	warn "fixing probable expired session";
      FIX: {
	  my $username   = $userdb->username_from_sessionid($id) or last FIX;
	  my $userid     = $userdb->get_user_id($username)       or last FIX;
	  my $uploadsid  = $userdb->get_uploads_id($userid)      or last FIX;
	  $session->uploadsid($uploadsid);
	  $userdb->set_session_and_uploadsid($userid,$session->id,$uploadsid);
	  $id = $session->id;
	}
    }

    my $nonce = Bio::Graphics::Browser2::Util->generate_id;
    my $ip    = CGI::remote_addr();

    $session->set_nonce($nonce,$ip,$remember);
    $session->username($username);
    $session->using_openid($using_openid);

    warn "id=$id, username =",$session->username if DEBUG;

    $session->flush();
    return ($session->id,$nonce);
}

sub background_track_render {
    my $self = shift;

    $self->session->unlock(); # don't hold session captive on renderers!
    
    $self->init_plugins();
    $self->init_remote_sources();

    $self->segment or return;
    my $cache_extra = $self->create_cache_extra;
    my $external    = $self->external_data;

    my $display_details   = 1;
    my $details_msg       = '';
    my %requests;

    if ( $self->get_panel_renderer($self->segment)->segment_length <= $self->get_max_segment ) {
        $requests{'detail'} =
            $self->render_deferred(
            labels          => [ $self->expand_track_names($self->detail_tracks) ],
            segment         => $self->segment,
            section         => 'detail',
            cache_extra     => $cache_extra,
            external_tracks => $external
            );
    } else {
        $display_details = 0;
        $details_msg = h1(
	    br(),
            $self->translate(
                'TOO_BIG',
                scalar $self->data_source()->unit_label($self->get_max_segment),
            )
        );
    }

    $requests{'region'} =
        $self->render_deferred( labels          => [ $self->expand_track_names($self->regionview_tracks) ],
				segment         => $self->region_segment,
				section         => 'region', 
				cache_extra     => $cache_extra, 
				external_tracks => $external,
	    )
        if ( $self->state->{region_size} && $self->data_source->show_section('region') );

    $requests{'overview'} =
        $self->render_deferred( labels          => [ $self->expand_track_names($self->overview_tracks) ],
				segment         => $self->whole_segment,
				section         => 'overview', 
				cache_extra     => $cache_extra, 
				external_tracks => $external,
	    )
        if ( $self->data_source->show_section('overview') );

    my (%track_keys,%seenit);
    for my $section (keys %requests) {
	my $cache_track_hash = $requests{$section};
        foreach my $track_label ( keys %{ $cache_track_hash || {} } ) {
	    my $track_id = $self->trackname_to_id($track_label,$section);
            $track_keys{ $track_id }
	      = $cache_track_hash->{$track_label}->key();
        }
    }

    warn "background_track_render() return track keys ",join ' ',%track_keys if DEBUG;

    return (\%track_keys, $display_details, $details_msg);
}

sub share_link {
    my $self = shift;
    my $file = shift;
    my $usertracks = $self->user_tracks;
    $usertracks->share_link($file);
    $self->add_user_tracks;
    my @tracks = $usertracks->labels($file);
    $self->add_track_to_state($_) foreach @tracks;
    return \@tracks;
}

sub add_tracks {
    my $self        = shift;
    my $track_names = shift;

    warn "[$$] add_tracks(@$track_names)" if DEBUG; 

    my %track_data;
    my $segment = $self->segment;
    
    my $remote;
    foreach (@$track_names) {
	$self->add_track_to_state($_);
	$remote++ if /http|ftp|das/;
	$remote++ if $self->data_source->setting($_=>'remote feature');
    }

    $self->init_remote_sources if $remote;
	
    if ($segment) {
	foreach my $track_name ( @$track_names ) {

	    my @track_ids = $self->expand_track_names($track_name);
	    
	    for my $track_id (@track_ids) {
	    
		warn "rendering track $track_id" if DEBUG;

		my ( $track_keys, $display_details, $details_msg )
		    = $self->background_individual_track_render($track_id);
	    
		my $track_key        = $track_keys->{$track_id};
		my $track_section    = $self->data_source->get_section_from_label($track_id);
		my $image_width      = $self->get_image_width($self->state);
		my $image_element_id = $track_name . "_image";
		
		my $track_html;
		if ( $track_section eq 'detail' and not $display_details ) {
		    my $image_width = $self->get_image_width($self->state);
		    $track_html .= $self->render_grey_track(
			track_id         => $track_name,
			image_width      => $image_width,
			image_height     => EMPTY_IMAGE_HEIGHT,
			image_element_id => $track_name . "_image",
			);
		}
		else {
		    $track_html = $self->render_deferred_track(
			cache_key  => $track_key,
			track_id   => $track_id,
			) || '';
		}
		$track_html = $self->wrap_track_in_track_div(
		    track_id   => $track_id,
		    track_name => $track_name,
		    track_html => $track_html,
		    );
	    
		my $panel_id = 'detail_panels';
		if ( $track_id =~ /:overview$/ ) {
		    $panel_id = 'overview_panels';
		}
		elsif ( $track_id =~ /:region$/ ) {
		    $panel_id = 'region_panels';
		}
		warn "add_track() returning track_id=$track_id, key=$track_key, name=$track_name, panel_id=$panel_id" if DEBUG;
		
		$track_data{$track_id} = {
		    track_key        => $track_key,
		    track_id         => $track_id,
		    track_name       => $track_name,
		    track_html       => $track_html,
		    track_section    => $track_section,
		    image_element_id => $image_element_id,
		    panel_id         => $panel_id,
		    display_details  => $display_details,
		    details_msg      => $details_msg,
		};
	    }
	}
    }	
    return \%track_data;
}

sub create_cache_extra {
    my $self     = shift;
    my $settings = $self->state();
    my @cache_extra = (
            $settings->{show_tooltips},
	    $settings->{ref},
            $settings->{start},
            $settings->{stop},
        );

    push @cache_extra,sort map {"$_\@$settings->{h_feat}{$_}"}
                               keys %{$settings->{h_feat}} 
                      if $settings->{h_feat};

    push @cache_extra,sort @{$settings->{h_region}}
                      if $settings->{h_region};

    push @cache_extra, map { $_->config_hash() } $self->plugins->plugins;

    return \@cache_extra;
}

sub background_individual_track_render {
    my $self    = shift;
    my $label   = shift;
    my $nocache = shift;

    my $display_details = 1;
    my $details_msg = '';

    my $external    = $self->external_data;
    my $source      = $self->data_source;
    
    my $section;
    my $segment;
    if ($label =~ /:overview$/){
        $section = 'overview';
        $segment = $self->whole_segment();
    }
    elsif ($label =~ /:region$/){
        $section = 'region';
        $segment = $self->region_segment();
    }
    else{
        $section = 'detail';
        $segment = $self->segment();
    }

    if ($section eq 'detail'
        && $self->segment
        && $self->get_panel_renderer($self->segment)->segment_length > $self->get_max_segment() )
    {
        $display_details = 0;
        $details_msg     = h1(
            $self->translate(
                'TOO_BIG',
                scalar $source->unit_label(MAX_SEGMENT),
            )
        );
        my %track_keys = ( $label => 0 );
        return ( \%track_keys, $display_details, $details_msg );
    }
    
    my $cache_extra = $self->create_cache_extra();

    # Start rendering the detail and overview tracks
    my $cache_track_hash = $self->render_deferred( 
	labels          => [ $label, ],
	segment         => $segment, 
        section         => $section, 
	cache_extra     => $cache_extra, 
	external_tracks => $external,
	nocache         => $nocache,
	);

    my %track_keys;
    foreach my $cache_track_hash ( $cache_track_hash, ) {
        foreach my $track_label ( keys %{ $cache_track_hash || {} } ) {
	    my $unique_label = 
		$source->is_remotetrack($track_label) ? $track_label
		: $external->{$track_label}           ? "$track_label:$section"
		: $track_label;
            $track_keys{ $unique_label }
                = $cache_track_hash->{$track_label}->key();
        }
    }

    return (\%track_keys, $display_details, $details_msg);
}

sub render {
  my $self           = shift;

  warn "[$$] render()" if DEBUG;

  # NOTE: these handle_* methods will return true
  # if they want us to exit before printing the header
  $self->handle_track_dump()         && return;
  $self->handle_gff_dump()           && return;
  $self->handle_plugins()            && return;
  $self->handle_download_userdata()  && return;

  $self->render_header();
  $self->render_body();
}

sub render_header {
  my $self    = shift;
  my $cookie1 = $self->state_cookie();
  my $cookie2 = $self->auth_cookie();
  my $header = CGI::header(
      -cache_control =>'no-cache',
      -cookie  => [$cookie1,$cookie2],
      -charset => $self->translate('CHARSET'),
  );
  print $header;
}

sub state_cookie {
  my $self    = shift;
  my $session = $self->session;
  my $id      = shift || $session->id;
  my $path    = url(-absolute => 1);
  $path       =~ s!gbrowse/?$!!;
  my $globals = $self->globals;
  my $cookie = CGI::Cookie->new(
      -name    => $CGI::Session::NAME,
      -value   => $id,
      -path    => $path,
      -httponly=> 1,
      -expires => '+'.$globals->time2sec($globals->remember_settings_time).'s',
      );
  return $cookie;
}

sub auth_cookie {
    my $self = shift;
    my $path = url(-absolute => 1);
    $path    =~ s!gbrowse/?$!!;
    my $auth     = (shift || param('authority')) or return;
    my $globals  = $self->globals;
    my $remember = $self->session->remember_auth;
    my @args = (-name => 'authority',
		-value=> $auth,
		-httponly=>1,
		-path => $path);
    if ($remember) {
	push @args,(-expires => '+'.$globals->time2sec($globals->remember_settings_time).'s');
    }
    return CGI::Cookie->new(@args);
}

# for backward compatibility
sub create_cookie { 
    my $self = shift;
    return [
	$self->state_cookie,
	$self->auth_cookie
	];
}

# For debugging
sub allparams {
  my $args = {};
  for my $key (param()) {
    $args->{$key} = param($key)
  }
  "<pre>".Data::Dumper::Dumper($args)."</pre>";
}
#print "All params:<br>\n".dumperonscreen($self->allparams);

sub render_body {
  my $self     = shift;

  warn "render_body()" if DEBUG;

  my $region   = $self->region;
  my $features = $region->features;
  my $settings = $self->state;
  my $source   = $self->data_source;

  my $title    = $self->generate_title($features);

  my $output;
  my @post_load = $self->get_post_load_functions;
  $output .= $self->render_html_start($title,@post_load);
  $output .= $self->render_user_header;
  $output .= $self->render_busy_signal;
  $output .= $self->render_actionmenu;
  $output .= $self->render_top($title,$features);

  my $main_page   .= $self->render_navbar($region->seg);

  if ($region->feature_count > 1) {
      $main_page .= $self->render_multiple_choices($features,$self->state->{name});
      $main_page .= $self->render_select_track_link;
  }

  elsif (my $seg = $region->seg) {
      $main_page .= $self->render_panels($seg,{overview   => $source->show_section('overview'),
					       regionview => $source->show_section('region'),
					       detailview => $source->show_section('detail')});
      $main_page .= $self->render_galaxy_form($seg);
  }

  elsif ($region->feature_count > 0) { # feature but no segment? Admin error
      my $message = $self->translate('CHROM_NOT_FOUND');
      my $details = $self->translate('CHROM_NOT_FOUND_DETAILS',  $features->[0]->display_name, $features->[0]->seq_id);
      $main_page .= script({-type=>'text/javascript'},"Controller.show_error('$message','$details')")
  }
  
  my $tracks        = $self->render_tracks_section;
  my $snapshots     = $self->snapshot_manager->render_snapshots_section;
  my $community     = $self->user_tracks->database? $self->render_community_tracks_section : "";
  my $custom        = $self->render_custom_tracks_section;
  my $global_config = $self->render_global_config;

  $output .= $self->render_tabbed_pages($main_page,$tracks,$snapshots,$community,$custom,$global_config);
  $output .= $self->login_manager->render_confirm;
  $output .= $self->render_bottom($features);

  print $output;
}

sub render_tracks_section {
    my $self = shift;
    return $self->render_toggle_track_table;
}

sub generate_title {
    my $self     = shift;
    my $features = shift;

    my $dsn         = $self->data_source;
    my $state       = $self->state;
    my $description = $dsn->description;
    my $divider     = $self->data_source->unit_divider;

    return $description unless $features;
    return !$features || !$state->{name}     ? $description
         : @$features == 0                   ? $self->translate('NOT_FOUND',$state->{name})
	 : @$features == 1 ? "$description: ".
				   $self->translate('SHOWING_FROM_TO',
					     scalar $dsn->unit_label($state->{view_stop} - $state->{view_start}+1),
					     $state->{ref},
					     $dsn->commas($state->{view_start}),
					     $dsn->commas($state->{view_stop}))
	 : $description;
}

# Provide segment info for rubberbanding and panning
sub segment_info_object {
    my $self          = shift;
    my $state         = $self->state;
    my $segment       = $self->segment;
    my $whole_segment = $self->whole_segment;

    my $renderer = $self->get_panel_renderer($segment);

    my $pad = $self->data_source->global_setting('pad_left')
        || $renderer->image_padding
        || 0;
    my $max = $self->get_max_segment;
    my $width = ( $state->{width} * OVERVIEW_RATIO );
    my $image_width  = $self->get_image_width($state);
    my $detail_width = $self->get_detail_image_width($state);

    my %segment_info_object = (
        image_padding        => $pad,
        max_segment          => $max,
        overview_start       => $whole_segment->start,
        overview_stop        => $whole_segment->end,
        overview_pixel_ratio => $whole_segment->length / $width,
        detail_start         => $segment->start,
        detail_stop          => $segment->end,
        'ref'                => $segment->seq_id,
        details_pixel_ratio  => $segment->length / ($state->{width} * $self->details_mult()),
        detail_width         => $detail_width,
        overview_width       => $image_width,
        width_no_pad         => $state->{width},
        details_mult         => $self->details_mult(),
        hilite_fill          => $self->data_source->global_setting('hilite fill')    || 'red',  # Not sure if there's a
        hilite_outline       => $self->data_source->global_setting('hilite outline') || 'gray', # better place for this
	hilite_height        => $self->data_source->global_setting('hilite height')  || '400px',
        flip                 => $state->{flip},
        initial_view_start   => $state->{view_start},
        initial_view_stop    => $state->{view_stop},
        length_label         => scalar $self->data_source->unit_label($state->{view_stop} - $state->{view_start}+1),
        description          => $self->data_source->description,
    );
    if ( $state->{region_size} ) {
        my ( $rstart, $rend ) = $self->regionview_bounds;
        my $rlen  = abs( $rend - $rstart );
        my $ratio = $rlen / $width;
        $segment_info_object{'region_start'}       = $rstart;
        $segment_info_object{'region_stop'}        = $rend;
        $segment_info_object{'region_pixel_ratio'} = $rlen / $width;
        $segment_info_object{'region_width'}       = $image_width;
    }
    return \%segment_info_object;
}

# Returns the HTML for the blank panel for a section (which will later be filled)
sub render_panels {
    my $self    = shift;
    my $seg     = shift;
    my $section = shift;

    warn "render_panels()" if DEBUG;

    my $html = '';

    my $cache_extra = $self->create_cache_extra();

    # Kick off track rendering
    if ($section->{'overview'} ) {
        my $scale_bar_html = $self->scale_bar( $seg, 'overview', );
        my $panels_html    = $self->get_blank_panels( [$self->overview_tracks],
						      'overview' );
	my $drag_script    = $self->drag_script( 'overview_panels', 'track' );
	$html .= div(
	    $self->toggle({tight=>1},
			  'Overview',
			  div({ -id => 'overview_panels', -class => 'track', -style=>'margin-bottom:3px; overflow: hidden; margin-left:auto; margin-right:auto; position:relative; width:'.$self->get_image_width($self->state).'px' },
			      $scale_bar_html, $panels_html,
			  ))
	    ) . $drag_script;
    }
    
    if ( $section->{'regionview'} and $self->state->{region_size} ) {

        my $scale_bar_html = $self->scale_bar( $seg, 'region' );
        my $panels_html    = $self->get_blank_panels( [$self->regionview_tracks],
						      'region');
        my $drag_script    = $self->drag_script( 'region_panels', 'track' );

        $html .= div(
            $self->toggle({tight=>1},
			  'Region',
			  div({ -id => 'region_panels', -class => 'track', 
				-style=>'margin-bottom:3px; overflow: hidden; margin-left:auto; margin-right:auto; position:relative; width:'.$self->get_image_width($self->state).'px'  },
			      $scale_bar_html, $panels_html,
			  )
            )
	    ) . $drag_script;
    }

    if ( $section->{'detailview'} ) {
        my $scale_bar_html = $self->scale_bar( $seg, 'detail' );
        my $ruler_html     = $self->render_ruler_div;
        my $panels_html    = $self->get_blank_panels( [$self->detail_tracks],
						      'detail');
        my $drag_script    = $self->drag_script( 'detail_panels', 'track' );
        my $details_msg    = span({ -id => 'details_msg', },'');
	my $clear_hilites  = $self->clear_highlights;
        $html .= div(
            $self->toggle({tight=>1},
			  'Details',
			  div({ -id => 'detail_panels', -class => 'track', -style=>'margin-left:auto; margin-right:auto; position:relative; width:'.$self->get_image_width($self->state).'px' },
			      $details_msg,
			      $ruler_html,
			      $scale_bar_html, 
			      $panels_html,
			  ),
			  div({-style=>'text-align:center'},
			      $self->render_select_track_link,
			      $clear_hilites,
			  ),
			  div($self->html_frag('html4',$self->state))
            )
	    ) . $drag_script;
    }
    return div({-id=>'panels'},$html);
}

sub get_post_load_functions {
    my $self = shift;
    my @fun;
    if (my $url = param('eurl')) {
	    my $trackname = $self->user_tracks->escape_url($url);
	    push @fun,'Controller.select_tab("custom_tracks_page")';
	    push @fun,"loadURL('$trackname','$url',true)";
    }
    return @fun;
}

sub scale_bar {
    my $self         = shift;
    my $seg          = shift;
    my $this_section = shift || 'detail';
    my $extra_args   = shift; 

    my $label = '';
    my ( $url, $height, $width );
    my $renderer = $self->get_panel_renderer($seg);
    if ( $this_section eq 'overview' ) {
        $label = OVERVIEW_SCALE_LABEL;
        ( $url, $height, $width ) = $renderer->render_scale_bar(
            section       => 'overview',
            whole_segment => $self->whole_segment,
            segment       => $seg,
            state         => $self->state
        );
    }
    elsif ( $this_section eq 'region' ) {
        $label = REGION_SCALE_LABEL;
        ( $url, $height, $width ) = $renderer->render_scale_bar(
            section        => 'region',
            region_segment => $self->region_segment,
            segment        => $seg,
            state          => $self->state
        );
    }
    elsif ( $this_section eq 'detail' ) {
        $label = DETAIL_SCALE_LABEL;
        ( $url, $height, $width ) = $renderer->render_scale_bar(
            section => 'detail',
            segment => $seg,
            state   => $self->state,
	    @$extra_args 
        );
    }
    my $html = $renderer->wrap_rendered_track(
        label      => $label,
        area_map   => [],
        width      => $width,
        height     => $height,
        url        => $url,
        status     => '',
	section    => $this_section,
    );
    $html = $self->wrap_track_in_track_div(
	track_name  => $label,
        track_id    => $label,
        track_html => $html,
        track_type => 'scale_bar',
    );
    return $html
}

sub init_database {
  my $self = shift;
  return $self->db() if $self->db();  # already done

  my $dsn = $self->data_source;
  my $db  = $dsn->open_database();

  # I don't know what this is for, 
  # but it was there in gbrowse and looks like an important hack.
  eval {$db->biosql->version($self->state->{version})};

  $self->db($db);
  $db;
}

sub name2segments {
    my $self = shift;
    my $name = shift;
    my $search   = $self->get_search_object();
    my @features = $search->search_features($name);
    return map {$search->segment($_)} map {$search->feature2segment($_)} @features;
}

sub region {
    my $self     = shift;

    return $self->{region} if $self->{region};

    my $source = $self->data_source;
    my $db     = $source->open_database();
    my $dbid   = $source->db2id($db);

    my $region   = Bio::Graphics::Browser2::Region->new(
 	{ source     => $self->data_source,
 	  state      => $self->state,
 	  db         => $db,
	  searchopts => $source->search_options($dbid),
	}
 	) or die;

    # run any "find" plugins
    my $plugin_action  = $self->plugin_action || '';
    my $current_plugin = $self->current_plugin;
    if ($current_plugin 
	&& $plugin_action eq $self->translate('Find')
	|| lc $plugin_action eq 'find') {
	$region->features($self->plugin_find($current_plugin,$self->state->{name}));
    }
    elsif ($self->state->{ref}) { # a known region
	$region->set_features_by_region(@{$self->state}{'ref','start','stop'});
    }
    elsif (my $features = $self->plugin_auto_find($self->state->{name})) {  # plugins with the auto_find() method defined
	$region->features($features);
    }
    else { # a feature search
	my $search   = $self->get_search_object();
	my $features = $search->search_features();
	my $max_feats = $self->data_source->global_setting('max keyword results');
	if ($features && @$features > $max_feats) {
	    $max_feats--;
	    @$features = $max_feats ?                # How many search results?
		         @{$features}[0..$max_feats] # max is > 1 
			 : ($features->[0]);         # max is 1
	}
	if ($@) {
	    (my $msg = $@) =~ s/\sat.+line \d+//;
	    $self->error_message($msg);
	    $self->state->{name} = ''; # to avoid the error again
	}
	$region->features($features);
    }

    $self->plugins->set_segments($region->segments) if $self->plugins;
    $self->state->{valid_region} = $region->feature_count > 0;
    return $self->{region}       = $region;
}

sub thin_segment {
    my $self  = shift;
    my $state = $self->state;
    if (defined $state->{ref}) {
	return Bio::Graphics::Feature->new(-seq_id => $state->{ref},
					   -start  => $state->{start},
					   -end    => $state->{stop});
    } else {
	return $self->segment;
    }
}

sub thin_whole_segment {
    my $self  = shift;

    my $state = $self->state;
    if (defined $state->{ref} && defined $state->{seg_min}) {
	return Bio::Graphics::Feature->new(-seq_id => $state->{ref},
					   -start  => $state->{seg_min},
					   -end    => $state->{seg_max});
    } else {
	return $self->whole_segment;
    }
}

sub thin_region_segment {
    my $self  = shift;
    my $state = $self->state;
    my $thin_segment       = $self->thin_segment;
    my $thin_whole_segment = $self->thin_whole_segment;

    return Bio::Graphics::Browser2::Region->region_segment(
	$thin_segment,
	$state,
	$thin_whole_segment);
}

sub segment {
    my $self   = shift;
    my $state  = $self->state;
    my $region = $self->region or return;
    return $region->seg;
}

sub whole_segment {
  my $self    = shift;

  return $self->{whole_segment} 
  if exists $self->{whole_segment};

  my $segment  = $self->segment;
  my $settings = $self->state;
  return $self->{whole_segment} = 
      $self->region->whole_segment($segment,$settings);
}

sub region_segment {
    my $self          = shift;
    return $self->{region_segment} 
       if exists $self->{region_segment};

    my $segment       = $self->segment;
    my $settings      = $self->state;
    return $self->{region_segment} = 
	$self->region->region_segment($segment,$settings,$self->whole_segment);
}

sub get_search_object {
    my $self = shift;
    return $self->{searchobj} if defined $self->{searchobj};
    my $search = Bio::Graphics::Browser2::RegionSearch->new(
	{ source => $self->data_source,
	  state  => $self->state,
	});
    $search->init_databases(
	param('dbid') ? [multi_param('dbid')]
	:()
	);
    return $self->{searchobj} = $search;
}

sub segment_length {
    my $self    = shift;
    my $label   = shift;
    my $segment = $self->segment;
    my $region  = $self->region_segment;
    my $whole   = $self->whole_segment;
    my $mult    = $self->details_mult;
    return $self->_segment_length($label,$segment,$region,$whole,$mult);
}

sub _segment_length {
    my $self = shift;
    my ($label,$segment,$region,$whole,$mult) = @_;

    my $section = $label 
	           ? Bio::Graphics::Browser2::DataSource->get_section_from_label($label) 
		   : 'detail';

    my $max = eval {$whole->length } || 9999999999;

    my $length = eval {$section eq 'detail'   ? ($segment->length+1)/$mult
		      :$section eq 'region'   ? $region->length
		      :$section eq 'overview' ? $whole->length
		      : 0} || 0;
    $length    = int($length+0.5);
    return $length > $max ? $max : $length;
}

# ========================= plugins =======================
sub init_plugins {
  my $self        = shift;
  my $source      = $self->data_source->name;
  return $PLUGINS{$source} if $PLUGINS{$source} && $self->{'.plugins_inited'}++;

  my @plugin_path = shellwords($self->data_source->globals->plugin_path);

  my $plugins = $PLUGINS{$source} ||= Bio::Graphics::Browser2::PluginSet->new($self->data_source,@plugin_path);
  $self->fatal_error("Could not initialize plugins") unless $plugins;
  $plugins->configure($self);
  $self->plugins($plugins);
  $self->load_plugin_annotators();
  $plugins;
}

# for activating plugins
sub plugin_action {
  my $self = shift;
  my $action;

  # the logic of this is obscure to me, but seems to have to do with activating plugins
  # via the URL versus via fill-out forms, which may go through a translation.
  if (param('plugin_do')) {
    $action = $self->translate(param('plugin_do')) || $self->translate('Go');
  }

  $action   ||=  param('plugin_action');
  $action   ||= 'find' if param('plugin_find');

  return $action;
}

sub current_plugin {
  my $self = shift;
  my $plugin_base = param('plugin') || param('plugin_find') or return;
  $self->plugins->plugin($plugin_base);
}

sub plugin_auto_find {
    my $self = shift;
    my $search_string = shift;
    return if Bio::Graphics::Browser2::Region->is_chromosome_region($search_string);
    my (@results,$found_one);
    my $plugins = $self->plugins or return;

    for my $plugin ($plugins->plugins) {  # not a typo
	next unless $plugin->type eq 'finder' && $plugin->can('auto_find');
	my $f = $plugin->auto_find($search_string);
	next unless $f;
	$found_one++;
	push @results,@$f;
    }
    return $found_one ? \@results : undef;
}

sub plugin_find {
  my $self = shift;
  my ($plugin,$search_string) = @_;

  my $settings    = $self->state;
  my $plugin_name = $plugin->name;
  my ($results,$keyword) = $plugin->can('auto_find') && defined $search_string
                             ? $plugin->auto_find($search_string)
                             : $plugin->find();

  # nothing returned, so plug the keyword into the search box to save user's search
  unless ($results && @$results) {
      $settings->{name} = $keyword ? $keyword : $self->translate('Plugin_search_2',$plugin_name);
      return;
  }

  # Write informative information into the search box - not sure if this is the right thing to do.
  $settings->{name} = defined($search_string) ? $self->translate('Plugin_search_1',$search_string,$plugin_name)
                                              : $self->translate('Plugin_search_2',$plugin_name);
  # do we really want to do this?!!
  $self->write_auto($results);
  return $results;
}

# Handle plug-ins that aren't taken care of asynchronously
# http://localhost/cgi-bin/gb2/gbrowse/yeast/?fetch=gff3           gff3 dump as text/plain
# http://localhost/cgi-bin/gb2/gbrowse/yeast/?fetch=save+gff3      gff3 dump as attachment
# http://localhost/cgi-bin/gb2/gbrowse/yeast/?fetch=save+gff3      gff3 dump as attachment
# http://localhost/cgi-bin/gb2/gbrowse/yeast/?fetch=gff3+trackdef  gff3 plus trackdefs
# add q=chr:start..end to filter by region (otherwise uses current session)
# add t=track1+track2+track3... to select tracks

sub handle_gff_dump {
    my $self = shift;

    my $gff_action;
    
    # new API
    if (my $action = param ('f') || param('fetch')) {
	$gff_action = $action;
    } elsif ($action = param('gbgff')||param('download_track')) {
	$gff_action  = 'scan'           if $action eq 'scan';
	$gff_action  = 'datafile'       if $action eq '1';
	$gff_action  = 'save datafile'  if $action =~ /save/i;
	$gff_action  = 'save fasta'     if $action =~ /fasta/i;
	$gff_action .= " trackdef"      if param('s') or param('stylesheet');
    }
    return unless $gff_action;

    my %actions = map {$_=>1} split /\s+/,$gff_action;

    my $segment    = param('q') || param('segment') || undef;

    my @labels     = $self->split_labels_correctly(param('l'));
    @labels        = $self->split_labels((param('type'),param('t'))) unless @labels;
    @labels        = $self->visible_tracks                           unless @labels;

    $self->state->{preferred_dump_format} = param('format') if param('format');

    my $dumper = Bio::Graphics::Browser2::TrackDumper->new(
        -data_source => $self->data_source(),
        -stylesheet  => $actions{trackdef}   ||  'no',
        '-dump'      => param('d')           || undef,
        -labels      => \@labels,
	-segment     => $segment             || undef,
        -mimetype    => param('m')           || undef,
	-format      => param('format')      || undef,
    ) or return 1;

    # so that another user's tracks are added if requested
    $self->add_user_tracks($self->data_source,param('uuid')) if param('uuid');

    my @l   = @labels;
    foreach (@l) {s/[^a-zA-Z0-9_]/_/g};

    my $title      = @l ? join('+',@l) : '';
    $title        .= $title ? "_$segment" : $segment if $segment;
    $title       ||= $self->data_source->name;

    if ($actions{scan}) {
	print header('text/plain');
	$dumper->print_scan();
    }
    else {
	$dumper->state($self->state);
	my $mime = $dumper->get_file_mime_type;
	my $ext  = $dumper->get_file_extension;

	if ($actions{save} && ($actions{datafile}||$actions{gff3})) {
	    print header( -type                => $mime,
			  -content_disposition => "attachment; filename=$title.$ext");
	    $dumper->print_datafile() ;
	}
	elsif ($actions{fasta}) {
	    my $build = $self->data_source->build_id;
	    $title   .= "_$build" if $build;
	    print header( -type                => $mime =~ /x-/ ? 'application/x-fasta' : $mime,
			  -content_disposition => "attachment; filename=$title.fa");
	    $dumper->print_fasta();
	}
	elsif ($actions{datafile}) {
	    print header( -type                => $mime);
	    $dumper->print_datafile();
	} elsif ($actions{trackdef}) {
	    print header( -type                => 'text/plain');
	    $dumper->print_stylesheet();
	} else {
	    print header( -type                => $mime,
			  -content_disposition => "attachment; filename=$title.$ext");
	    $dumper->print_datafile() ;
	}
    }

    return 1;
}

sub track_filter_plugin {
    my $self = shift;
    my $plugins  = $self->plugins;
    my ($filter) = grep {$_->type eq 'trackfilter'} $plugins->plugins;
    return $filter;
}

# track dumper
sub handle_track_dump {
    my $self   = shift;
    my $source = $self->data_source;

    param('show_tracks') or return;
    print header('text/plain');
    
    my (%ts,%ds,@labels_to_dump);
    if (my @labels = $source->track_source_to_label(shellwords param('ts'))) {
	%ts     = map {$_=>1} @labels;
    }
    if (my @labels = $source->data_source_to_label(shellwords param('ds'))) {
	%ds     = map {$_=>1} @labels;
    }
    if (param('ts') && param('ds')) { # intersect
	@labels_to_dump = grep {$ts{$_}} keys %ds;
    } elsif (param('ts') or param('ds')) { #union
	@labels_to_dump = (keys %ts,keys %ds);
    } else {
	@labels_to_dump = $source->labels;
    }

    print '#',join("\t",qw(TrackLabel DataSource TrackSource Description)),"\n";
    for my $l (@labels_to_dump) {
	next if $l =~ /_scale/;
	next if $l =~ /(plugin|file):/;
	print join("\t",
		   $l,
		   $source->setting($l=>'data source'),
		   $source->setting($l=>'track source'),
		   $source->setting($l=>'key')),"\n";
    }
    return 1;
}

# Handle plug-ins that aren't taken care of asynchronously
sub handle_plugins {
    my $self = shift;

    my $plugin_base = param('plugin');
    return unless ($plugin_base);

    $self->init_plugins();
    my ($id) = $plugin_base =~ /^plugin:(\w+)/;
    $id    ||= $plugin_base;
    my $plugin      = $self->plugins->plugin($id);
    warn "an operation was requested on plugin $id, but this plugin has not been configured"
	unless $plugin;
    return unless $plugin;

    my $plugin_type = $plugin->type();

    my $plugin_action = param('plugin_action') || '';

    # for activating the plugin by URL
    if ( param('plugin_do') ) {
        $plugin_action = $self->translate( param('plugin_do') ) || $self->translate('Go');
    }
	
    my $state  = $self->state();
    my $cookie = $self->create_cookie();

    ### CONFIGURE  ###############################################
    if ($plugin_action eq $self->translate('Configure')) {
	$self->plugin_configuration_form($plugin);
	return 1;
    }
    

    ### FIND #####################################################
    if ( $plugin_action eq $self->translate('Find') ) {
        #$self->do_plugin_find( $state, $plugin_base, $features )
        #    or ( $plugin_action = 'Configure' );    #reconfigure
        return;
    }

    ### DUMP #####################################################
    # Check to see whether one of the plugin dumpers was invoked.  We have to
    # do this first before printing the header because the plugins are
    # responsible for generating the header.  NOTE THE return 1 HERE IF THE
    # DUMP IS SUCCESSFUL!

    my $segment = $self->segment();
    if (    $plugin_type   eq 'dumper'
        and $plugin_action eq $self->translate('Go')
        and (  $segment
            or param('plugin_config')
            or $plugin->verb eq ( $self->translate('Import') || 'Import' ) )
        )
    {
        $segment->{start} = param('view_start') || $segment->{start}; # We only care about the segment the user is actually
        $segment->{stop}  = param('view_stop')  || $segment->{stop};  # viewing, not the whole segment that he/she has loaded
        $segment->{end}   = param('view_stop')  || $segment->{end};   #

	my $search      = $self->get_search_object();
	my $metasegment = $search->segment($segment);
        $self->do_plugin_header( $plugin, $cookie );
        $self->do_plugin_dump( $plugin, $metasegment, $state )
            && return 1;
    }

    if ($plugin_type eq 'annotator' && $plugin_action eq $self->translate('Go')) {
	warn "HERE I AM";
	return 1;
    }

    return;
}

sub do_plugin_header {
    my $self   = shift;
    my $plugin = shift;
    my $cookie = shift;

    # Defined in HTML.pm
}

sub do_plugin_dump {
    my $self    = shift;
    my $plugin  = shift;
    my $segment = shift;
    my $state   = shift;
    my @additional_feature_sets;

    $plugin->dump( $segment, @additional_feature_sets );
    return 1;
}

#======================== remote sources ====================
sub init_remote_sources {
  my $self = shift;
  warn "init_remote_sources()" if DEBUG;
  my $remote_sources   = Bio::Graphics::Browser2::RemoteSet->new($self->data_source,
								 $self->state,
								 $self->language,
								 $self->session->uploadsid,
      );
  $remote_sources->add_files_from_state;
  $self->remote_sources($remote_sources);
  return $remote_sources;
}

# this generates the form that is sent to Galaxy
# defined in HTML.pm
sub render_galaxy_form {
    my $self = shift;
    my $seg  = shift;
    $self->wrap_in_div('galaxy_form',
		     $self->galaxy_form($seg));
}

# to be inherited
sub galaxy_form { }

sub delete_uploads {
    my $self = shift;
    my $userdata = $self->user_tracks;
    my @files  = $userdata->tracks;
    for my $file (@files) {
	my @tracks = $userdata->labels($file);
	$userdata->delete_file($file);
	$self->remove_track_from_state($_) foreach @tracks;
    }
    $self->data_source->clear_usertracks();
}

sub cleanup {
  my $self = shift;
  warn "cleanup()" if DEBUG;
  $self->plugins->destroy;
  my $state = $self->state;
  $state->{name} = $self->region_string if $state->{ref};  # to remember us by :-)
}

sub add_remote_tracks {
    my $self        = shift;
    my $urls        = shift;

    my $user_tracks = $self->user_tracks;

    warn "ADD_REMOTE_TRACKS(@$urls)" if DEBUG;

    my @tracks;

    for my $url (@$urls) {
	my $name = $user_tracks->create_track_folder($url);
	my ($result,$msg,$tracks) 
	    = $user_tracks->mirror_url($name,$url,1,$self);
	warn "[$$] $url: result=$result, msg=$msg, tracks=@$tracks" if DEBUG;
	push @tracks,@$tracks;
    }

    push @tracks,$self->add_user_tracks($self->data_source);
    warn "[$$] adding tracks @tracks" if DEBUG;
    $self->add_track_to_state($_) foreach @tracks;
    $self->init_remote_sources();
}

sub write_auto {
    my $self             = shift;
    my $features         = shift;
    my $styles           = shift;
    my $setting          = $self->state();
    return unless @$features;

    my $user_tracks = $self->user_tracks;

    # ideally we should fork here and pass our child's STDOUT
    # to $user_tracks->upload_file(), but I'm scared...
    # So we create the file in core.
    my $feature_file = '';

    $styles ||= [];
    for my $style (@$styles) {
        my ( $type, @options ) = shellwords($style);
        $feature_file .= "[$type]\n";
        $feature_file .= join "\n", @options;
        $feature_file .= "\n";
    }

    my %seenit;
    for my $f (@$features) {
        my $reference = $f->can('seq_id') ? $f->seq_id : $f->seq_id;
        my $type      = $f->primary_tag;
        my $name      = $f->seqname;
        my $position
            = $f->sub_SeqFeature
            ? join( ',',
            map { $_->strand >= 0 ? $_->start . '..' . $_->end : $_->end . '..' . $_->start } $f->sub_SeqFeature )
            : $f->start . '..' . $f->end;
        $name .= "($seenit{$name})" if $seenit{$name}++;
        $feature_file .= "\nreference=$reference\n";
        $feature_file .= join( "\t", qq("$type"), qq("$name"), $position ). "\n";
    }

    my ($result,$msg,$tracks) 
	= $user_tracks->upload_data('My Track',$feature_file,'text/plain','overwrite');
    warn $msg unless $result;

    $self->add_user_tracks($self->data_source);
    $self->add_track_to_state($_) foreach @$tracks;
}

sub handle_download_userdata {
    my $self = shift;
    my $ftype   = param('userdata_download')    or return;
    my $file   = param('track')                or return;

    my $userdata = $self->user_tracks;
    my $download = $ftype eq 'conf' ? $userdata->track_conf($file)
	                        : $userdata->data_path($file, $ftype);

    my $fname = basename($download);
    my $is_text = -T $download;

    print CGI::header(-attachment   => $fname,
		      -charset      => $self->translate('CHARSET'), # 'US-ASCII' ?
		      -type         => $is_text ? 'text/plain' : 'application/octet-stream');

    my $f = $ftype eq 'conf' ? $userdata->conf_fh($file)
	                     : IO::File->new($download);
    $f or croak "$download: $!";

    if ($is_text) {
	# try to make the file match the native line endings
	# not necessary?
	my $eol = $self->guess_eol();
	while (<$f>) {
	    # print;
	    s/[\r\n]+$//;
	    print $_,$eol;
	}
    } else {
	my $buffer;
	while (read($f,$buffer,1024)) {
	    print $buffer;
	}
    }
    close $f;
    return 1;
}

sub guess_eol {
    my $self = shift;
    my $agent = CGI->user_agent;
    return "\012"     if $agent =~ /linux/i;
    return "\015"     if $agent =~ /macintosh/i;
    return "\015\012" if $agent =~ /windows/i;
    return "\n"; # default
}

sub handle_quickie {
    my $self   = shift;
    my $data   = shift;
    my $styles = shift;
    return unless $data;

    # format of quickie data is
    # reference+type+name+start..end,start..end,start..end
    my @features;
    foreach my $d (@$data) {
        my ( $reference, $type, $name, @segments )
            = $self->parse_feature_str($d);
        push @features,
            Bio::Graphics::Feature->new(
            -ref  => $reference || '',
            -type => $type      || '',
            -name => $name      || '',
            -segments => \@segments,
            );
    }
    $self->write_auto( \@features, $styles );
}

sub parse_feature_str {
    my $self = shift;
    my $f    = shift;
    my ( $reference, $type, $name, @position );
    my @args = shellwords( $f || '' );
    if ( @args > 3 ) {
        ( $reference, $type, $name, @position ) = @args;
    }
    elsif ( @args > 2 ) {
        ( $reference, $name, @position ) = @args;
    }
    elsif ( @args > 1 ) {
        ( $reference, @position ) = @args;
    }
    elsif ( $f =~ /^(.+):(\d+.+)$/ ) {
        ( $reference, @position ) = ( $1, $2 );
    }
    elsif ( $f =~ /^(.+)/ ) {
        $reference = $1;
        @position  = '1..1';
    }
    return unless $reference;

    $type = 'region' unless defined $type;
    $name = "Feature " . ++$self->{added_features} unless defined $name;

    my @segments
        = map { [/(-?\d+)(?:-|\.\.)(-?\d+)/] } map { split ',' } @position;
    ( $reference, $type, $name, @segments );
}

###################################################################################
#
# SETTINGS CODE HERE
#
###################################################################################

sub globals {
  my $self = shift;
  $self->data_source->globals;
}

# the setting method either calls the DATA_SOURCE's global_setting or setting(), depending
# on the number of arguments used.
sub setting {
  my $self = shift;
  my $data_source = $self->data_source;

  if (@_ == 1) {
    return $data_source->global_setting(@_);
  }

  else {
    # otherwise we get the data_source-specific settings
    return $data_source->setting(@_);
  }
}

=head2 plugin_setting()

   $value = = $browser->plugin_setting("option_name");

When called in the context of a plugin, returns the setting for the
requested option.  The option must be placed in a [PluginName:plugin]
configuration file section:

  [MyPlugin:plugin]
  foo = bar

Now within the MyPlugin.pm plugin, you may call
$browser->plugin_setting('foo') to return value "bar".

=cut

sub plugin_setting {
  my $self           = shift;
  my $caller_package = caller();
  my ($last_name)    = $caller_package =~ /(\w+)$/;
  my $option_name    = "${last_name}:plugin";
  $self->setting($option_name => @_);
}

# dealing with external DAS sources?
sub get_external_presets {
  my $self = shift;
  my $presets  = $self->setting('remote sources') or return;
  my @presets  = shellwords($presets||'');
  my (@labels,@urls);
  while (@presets) {
    my ($label,$url) = splice(@presets,0,2);
    next unless $url && $url =~ /^(http|ftp)/;
    push @labels,$label;
    push @urls,$url;
  }
  return unless @labels;
  return (\@labels,\@urls) if wantarray;
  my %presets;
  @presets{@urls} = @labels;
  return \%presets;
}

##################################################################3
#
# AUTHENTICATION
#
##################################################################3
sub force_authentication {
    my $self = shift;

    # asynchronous event -- only allow the ones needed for authentication
    if (Bio::Graphics::Browser2::Action->is_authentication_event) {
	$self->run_asynchronous_event;
	$self->session->unlock;
	return;
    }

    if (param('action')) {
	print CGI::header(-status => '403 Forbidden');
	return;
    }

    # render main page
    $self->init();
    $self->render_header();

    my $confirm        = param('confirm') || param('openid_confirm');

    my $action;
    if ($self->data_source->auth_plugin) {
	$action = "GBox.showTooltip(event,'url:?action=plugin_login',true)";
    } else {
 	$action = $self->login_manager->login_script();
    }
    $action .= ";login_blackout(true,'')";

    my $output = $self->render_html_start('GBrowse Login Required',
					  $self->get_post_load_functions,
					  ($confirm ? '' : $action));
    $output .= div({-id=>'source_form'},$self->source_form());
    if ($confirm) {
	$output .= $self->login_manager->render_confirm;
    } else {
	$output .= $self->render_login_required($action);
    }
    $output .= "<hr>";
    $output .= $self->render_bottom();
    print $output;
}

##################################################################3
#
# STATE CODE HERE
#
##################################################################3

sub set_default_state {
  my $self = shift;
  my $state = $self->state;
  $self->default_state if !$state->{tracks} # always set in one form or another
                           or param('reset');
}

sub update_state {
  my $self   = shift;
  warn "[$$] update_state()" if DEBUG;
  return if param('gbgff');          # don't let gbgff requests update our coordinates!!!
#  return if url() =~ /gbrowse_img/;  # don't let gbrowse_img requests update our coordinates either!!
  $self->_update_state;
}

sub _update_state {
    my $self = shift;

    my $state  = $self->state;

    $self->update_state_from_cgi;
    warn "[$$] CGI updated" if DEBUG;
    if (my $seg = $self->segment) {
	# A reset won't have a segment, so we need to test for that before we use
	# one in whole_segment().
	my $whole_segment = $self->whole_segment;
	$state->{seg_min} = $whole_segment->start;
	$state->{seg_max} = $whole_segment->end;
	
	$state->{ref}          ||= $seg->seq_id;
	$state->{view_start}   ||= $seg->start; # The user has selected the area that they want to see. Therefore, this
	$state->{view_stop}    ||= $seg->end;   # will be the view_start and view_stop, rather than just start and stop.
	                                        # asynchronous_update_coordinates will multiply this by the correct factor
	                                        # to find the size of the segment to load

	$state->{start}   ||= $seg->start;      # Set regular start and stop as well, just to be safe
	$state->{stop}    ||= $seg->end;        # 
	
	# Automatically open the tracks with found features in them
	$self->auto_open();
    }
    $self->cleanup_dangling_uploads($state);
    warn "[$$] update_state() done" if DEBUG;
}

sub default_state {
  my $self  = shift;
  my $state = $self->state;
  my $data_source = $self->data_source;
  %$state = ();
  @$state{'name','ref','start','stop','flip','version'} = ('','','','','',100);
  $state->{width}        = $self->setting('default width');
  $state->{source}       = $data_source->name;
  $state->{cache}        = $data_source->cache_time>0;
  $state->{region_size}  = $self->setting('region segment');
  $state->{'max segment'}= $self->setting('max segment');
  $state->{v}            = VERSION;
  $state->{stp}          = 1;
  $state->{ins}          = 1;
  $state->{head}         = 1;
  $state->{show_tooltips}= 1;
  $state->{ks}           = 'between';
  $state->{grid}         = 1;
  $state->{sk}           = $self->setting("default varying") ? "unsorted" : "sorted";

  # if no name is specified but there is a "initial landmark" defined in the
  # config file, then we default to that.
  $state->{name} ||= $self->setting('initial landmark') 
    if defined $self->setting('initial landmark');

  $self->default_tracks();
  $self->default_category_open();
  $self->session->unlock();
}

sub default_category_open {
    my $self = shift;
    my $state = $self->state;

    my $categories = $self->data_source->category_open;
    for my $c (keys %$categories) {
	$state->{section_visible}{$c} = $categories->{$c};
    }
}

sub default_tracks {
  my $self  = shift;
  my $state  = $self->state;
#  my @labels = $self->data_source->labels;
  my @labels = $self->potential_tracks;

  $state->{tracks}   = \@labels;

  $state->{features}{$_} = {visible=>0,options=>0,limit=>0}
      foreach @labels;
  $state->{features}{$_}{visible} = 1
      foreach $self->data_source->default_labels;

  # set collapse state here
  my $source = $self->data_source;
  for my $label (@labels) {
      my $visibility = $source->setting($label => 'visible');
      next unless defined $visibility;
      $visibility = lc $visibility;
      $state->{features}{$label}{visible} = 1 if $visibility eq 'show'
	                                      or $visibility eq 'collapse';
      $state->{features}{$label}{visible} = 0 if $visibility eq 'hide';
      $state->{track_collapsed}{$label}   = 1 if $visibility eq 'collapse';
  }
}

# Open Automatically the tracks with features in them
sub auto_open {
    my $self     = shift;
    my $features = $self->region()->features() || return;
    @$features  <= 1 or return;  # don't autoopen multiple hits!!
    my $state    = $self->state;

    for my $feature (@$features) {
	# the next step optimizes away the case in which the feature type
	# is a chromosome region
	next if $feature->type eq 'region' || $feature->type eq 'segment';

        my @desired_labels = $self->data_source()->feature2label($feature);
	@desired_labels || next ;
	warn "desired labels = @desired_labels" if DEBUG;
	for my $desired_label (@desired_labels) {
	    warn "auto_open(): add_track_to_state($desired_label)" if DEBUG;
	    $self->add_track_to_state($desired_label);
	    $state->{h_feat} = {};
	    my $h_color = $self->data_source->global_setting('hilite color') || 'yellow';
	    $state->{h_feat}{ lc $feature->display_name } = $h_color
		unless param('h_feat') && param('h_feat') eq '_clear_';
	}
    }
}

# remove upload markers that are no longer relevant
sub cleanup_dangling_uploads {
    my $self  = shift;
    my $state = shift;
	
    my %name_to_id;
    for my $id (keys %{$state->{uploads}}) {
		unless ($state->{uploads}{$id}[0]) {
			delete $state->{uploads}{$id};
			next;
		}
		$name_to_id{$state->{uploads}{$id}[0]}{$id}++;
    }

    my $usertracks = $self->user_tracks;
    my %tracks = map {$_=>1} $usertracks->tracks();

    for my $k (keys %name_to_id) {
		unless (exists $tracks{$k}) {
			delete $state->{uploads}{$_} foreach keys %{$name_to_id{$k}};
		}
    }

}

sub add_track_to_state {
  my $self  = shift;
  my $label = shift;
  my $state = $self->state;

  warn '[',Bio::Graphics::Browser2::Session->time,'] ',"[$$] add_track_to_state($label)" if DEBUG;

  return unless length $label; # refuse to add empty tracks!

  # don't add invalid track
  my %potential_tracks = map {$_=>1} $self->potential_tracks;
  warn "invalid track $label" if DEBUG && !$potential_tracks{$label};
  return unless $potential_tracks{$label};

#  my %current = map {$_=> 1} @{$state->{tracks}};
#  unshift @{$state->{tracks}},$label unless $current{$label}; # on top (better)

  # experimental -- force track to go to top
  @{$state->{tracks}} = grep {$_ ne $label} @{$state->{tracks}};
  unshift @{$state->{tracks}},$label;

  warn "[$$]ADD TRACK TO STATE WAS: ",
    join ' ',grep {$state->{features}{$_}{visible}} sort keys %{$state->{features}},"\n" if DEBUG;

  if ($state->{features}{$label}) {
    $state->{features}{$label}{visible}=1;
  }
  else{
    $state->{features}{$label}{visible} = {visible=>1,options=>0,limit=>0};
  }

  warn "[$$] ADD TRACK TO STATE NOW: ",
    join ' ',grep {$state->{features}{$_}{visible}} sort keys %{$state->{features}},"\n" if DEBUG;
}

sub remove_track_from_state {
  my $self  = shift;
  my $label = shift;
  warn '[',Bio::Graphics::Browser2::Session->time,'] ',"[$$] remove_track_from_state($label)" if DEBUG;
  delete $self->state->{features}{$label};
}

sub track_visible {
    my $self  = shift;
    my $label = shift;
    return $self->state->{features}{$label}{visible};
}

sub update_state_from_cgi {
  my $self  = shift;
  my $state = $self->state;
  warn "state = $state" if DEBUG;
  $self->update_options($state);
  $self->update_coordinates($state);
  $self->update_region($state);

  if (param('revert')) {
      $self->default_tracks($state);
  }
  else {
      $self->remove_invalid_tracks($state);
      $self->update_tracks($state);
  }

  $self->update_section_visibility($state);
  $self->update_galaxy_url($state);
}

sub create_subtrack_manager {
    my $self          = shift;
    my $label         = shift;
    my $source        = shift || $self->data_source;
    my $state         = shift || $self->state;
    
    my ($dimensions,$rows,$aliases) 
	= Bio::Graphics::Browser2::SubtrackTable->infer_settings_from_source($source,$label)
	or return;

    my $key            = $source->setting($label => 'key');
    my $selected       = $state->{subtracks}{$label};
    my $comment        = $source->setting($label => 'brief comment');

    my $stt            = Bio::Graphics::Browser2::SubtrackTable->new(-columns=>$dimensions,
								     -rows   =>$rows,
								     -label  => $label,
								     -key    => $key||$label,
								     -aliases => $aliases,
								     -comment => $comment);
    $stt->set_selected($selected) if $selected;
    eval {$stt->overlap(($state->{features}{$label}{options}||0)==4)};  # options == 4 means "overlap", for legacy reasons
    return $stt;
}

# Handle returns from the track configuration form
sub reconfigure_track {
    my $self  = shift;

    my $q     = shift; # CGI object
    my $label = shift;

    my $state  = $self->state();
    my $source = $self->data_source;

    $state->{features}{$label}{options}  = $q->param('format_option');
    my $dynamic = $self->translate('DYNAMIC_VALUE');
    my $mode    = $q->param('mode') || 'details';

    my $length            = ($q->param('segment_length')||0);
    my $semantic_low      = ($q->param('apply_semantic_low')||0);
    my $semantic_hi       = ($q->param('apply_semantic_hi')||0)   || $self->get_max_segment;
    my $delete_semantic   = $q->param('delete_semantic');
    my $summary           = $q->param('summary_mode');

    $state->{features}{$label}{summary_mode_len} = $summary if defined $summary;

    ($semantic_low,$semantic_hi) = ($semantic_hi,$semantic_low) if $semantic_low > $semantic_hi;
    $self->clip_override_ranges($state->{features}{$label}{semantic_override},
				$semantic_low,
				$semantic_hi)
	if $length;

    my $o = $mode eq 'summary' ? $state->{features}{$label}{summary_override}                                ||= {}
                               : $state->{features}{$label}{semantic_override}{"$semantic_low:$semantic_hi"} ||= {};

    my $glyph = $q->param('conf_glyph') || '';
    for my $s ( grep {/^conf_/} $q->param()) {
        my @values = $q->param($s);
	my $value  = $values[-1]; # last one wins
	$s =~ s/^conf_//;
	next unless defined $value;

	if ($s =~ /(\w+)_subtype/) {
	    next unless $1 eq $glyph;
	    $s = 'glyph_subtype';
	}
	elsif ($s =~ /(\w+)_graphtype/) {
	    next unless $1 eq $glyph;
	    $s = 'graph_type';
	} elsif ($s =~ /(\w+)_autoscale/) {
	    my $g = $1;
	    next if $g eq 'wiggle' && $glyph !~ /wiggle|vista|hybrid/;
	    next if $g eq 'xyplot' && $glyph !~ /xyplot|density|hybrid/;
	    $s = 'autoscale';
	}

	# semantic setting for this configured length
	my $configured_value = $source->semantic_fallback_setting($label=>$s,$semantic_low+1);

	if ($value eq $dynamic) {
	    delete $o->{$s};
	} elsif ($s eq 'bicolor_pivot' && $value eq 'value') {
	    my $bp = $q->param('bicolor_pivot_value');
	    $o->{$s} =    $bp if !defined $configured_value or $bp != $configured_value;
	} else {
	    $o->{$s} = $value if !defined $configured_value or !defined $o->{$s} or $value ne $configured_value;
	    if ($glyph eq 'wiggle_whiskers') {# workarounds for whisker options
		$o->{"${s}_neg"}  = $value if $s =~ /^(mean_color|stdev_color)/; 
		$o->{min_color}   = $value if $s eq 'max_color';
	    }
	}
    }
    if (defined $o->{autoscale} && $o->{autoscale}=~/local|global|chromosome/) { 
	undef $o->{min_score}; 
	undef $o->{max_score} 
    }
}

#        low                    hi
#          <--------------------->  current
#  <-----------> A
#                            <-------------> B
#   <--------------------------------------------->  C
#                <--------->  D
#
sub clip_override_ranges {
    my $self = shift;
    my ($semconf,$low,$hi) = @_;

    # legacy representation of bounds
    for my $k (keys %$semconf) {
	unless ($k =~ /:/) {
	    $semconf->{"$k:999999999"} = $semconf->{$k};
	    delete $semconf->{$k};
	}
    }

    my @ranges = map {
	my ($l,$h) = split ':';
	$l ||= 0;
	$h ||= 1_000_000_000;
	[$l,$h];
    } keys %$semconf;
    @ranges = sort {$a->[0]<=>$b->[0]} @ranges;
    for my $r (@ranges) {
	my $key  = "$r->[0]:$r->[1]";
	my $conf = $semconf->{$key};
	delete $semconf->{$key};
	my $overlap;

	if ($r->[0] <= $low && $r->[1] >= $hi) {   # case C
	    $semconf->{$r->[0]  . ':' . ($low-1)} = $conf unless $r->[0] >= $low-1;
	    $semconf->{$hi      . ':' . $r->[1] } = $conf unless $hi     >= $r->[1];
	    $overlap++;
	}

	if ($r->[0] > $low && $r->[1] < $hi) {   # case D
	    $overlap++;
	    # delete
	}
	
	if ($r->[1] >= $low && $r->[0] <= $low) { # case A
	    $r->[1] =  $low-1;
	    $semconf->{"$r->[0]:$r->[1]"} = $conf
		unless $r->[0] >= $r->[1];
	    $overlap++;
	} 

	if ($r->[1] >= $hi && $r->[0] <= $hi) {   # case B
	    $r->[0] =  $hi;
	    $semconf->{"$r->[0]:$r->[1]"} = $conf
		unless $r->[0] >= $r->[1];
	    $overlap++;
	}

	unless ($overlap) {
	    $semconf->{$key} = $conf;
	}
    }
}

sub find_override_bounds {
    my $self = shift;
    my ($semconf,$length) = @_;
    my @ranges = sort {$a->[0]<=>$b->[0]} 
    map { my @a = split ':';
	  \@a
    } keys %$semconf;
    my ($low,$hi);
    for my $r (@ranges) {
	next unless @$r == 2;
	if ($length >= $r->[0] && $length < $r->[1]) {
	    return @$r;
	}
	$low = $r->[1]+1 if $r->[1] < $length;
	$hi  = $r->[0]-1 if $r->[0] > $length;
    }
    return ($low,$hi);
}

sub find_override_region {
    my $self = shift;
    my ($semconf,$length) = @_;
    my @ranges = keys %$semconf;
    for my $r (@ranges) {
	my ($low,$hi) = split ':',$r;
	return $r if $length >= $low && (!defined $hi || $length < $hi);
    }
    return;
}

sub update_options {
  my $self  = shift;
  my $state = shift || $self->state;
  my $data_source = shift || $self->data_source;

  #  return unless param('width'); # not submitted
  $state->{width} ||= $self->setting('default width');  # working around a bug during development

  $state->{grid} = 1 unless exists $state->{grid};  # to upgrade from older settings
  $state->{flip} = 0;  # obnoxious for this to persist

  $state->{version} ||= param('version') || '';
  do {$state->{$_} = param($_) if defined param($_) } 
    foreach qw(name source plugin stp ins head ks sk version 
               grid flip width region_size show_tooltips cache
               );

  if (my @features = shellwords(multi_param('h_feat'))) {
      $state->{h_feat} = {};
      for my $hilight (@features) {
	  last if $hilight eq '_clear_';
	  my ($featname,$color) = split '@',$hilight;
	  $state->{h_feat}{lc $featname} = $color || 'yellow';
      }
  }

  if (my @regions = shellwords(multi_param('h_region'))) {
      $state->{h_region} = [];
      foreach (@regions) {
	  last if $_ eq '_clear_';
	  $_ = "$state->{ref}:$_" unless /^[^:]+:-?\d/; # add reference if not there
	  push @{$state->{h_region}},$_;
      }
  }

  # Process the magic "q" parameter, which overrides everything else.
  if (my @q = multi_param('q')) {
    delete $state->{$_} foreach qw(name ref h_feat h_region);
    $state->{q} = [map {split /[+-]/} @q];
  }

  else  {
    $state->{name} ||= '';
    $state->{name} =~ s/^\s+//; # strip leading
    $state->{name} =~ s/\s+$//; # and trailing whitespace
  }
  $self->session->modified;
}

sub update_tracks {
  my $self  = shift;
  my $state = shift;

  if (my @add = multi_param('add')) {
      my @style = multi_param('style');
      $self->handle_quickie(\@add,\@style);
  }

  # selected tracks can be set by the 'l', 'label' or 't' parameter
  # the preferred parameter is 'l', because it implements correct
  # semantics for the label separator
  if (my @l = multi_param('l')) {
      $self->set_tracks($self->split_labels_correctly(@l));
  }
  elsif (@l = multi_param('label')) {
      $self->set_tracks($self->split_labels(@l));
  } #... the 't' parameter
  elsif (my @t = multi_param('t')) {
      $self->set_tracks($self->split_labels(@t));
  } #... the 'ds' (data source) or the 'ts' (track source) parameter
  elsif ((my @ds = shellwords multi_param('ds')) || (my @ts = shellwords multi_param('ts'))) {
      my @main_l = @ds ? $self->data_source->data_source_to_label(@ds) : $self->data_source->track_source_to_label(@ts);
      if (!@ds && @ts) {
       my %ds = ();
       foreach my $label (@main_l) {
	   my @tracks = grep {!/^#/} shellwords $self->setting($label=>'track source');
	   my @datasr = grep {!/^#/} shellwords $self->setting($label=>'data source');

	   for (my $i = 0; $i <@tracks; $i++) {
	       map{$ds{$datasr[$i]}++ if $_ == $tracks[$i] && $datasr[$i]} (@ts);
	   }
       }
       @ds = keys %ds;
      }

      foreach my $label (@main_l) {
	  my @subs = grep {!/^#/} shellwords $self->setting($label=>'select');
	  shift @subs;
	  
	  my @matched;
	  foreach my $s (@subs) {
	      map {push(@matched,$`) if ($s=~/\D(\d+)\;*$/i && $1 == $_)} @ds;
	      map {s/\s*//} @matched;													#**
	  }
	  $label.="/".join("+",@matched) if @matched;
      }
      
      $self->set_tracks(@main_l);
  }
  
  if (my @selected = $self->split_labels_correctly(multi_param('enable'))) {
      $self->add_track_to_state($_) foreach @selected;
  }
  
  if (my @selected = $self->split_labels_correctly(multi_param('disable'))) {
      $self->remove_track_from_state($_) foreach @selected;
  }
  
}

# update coordinates logic
# 1. A fresh session will have a null {ref,start,stop} state, a previous session will have {ref,start,stop,seg_min,seg_max} defined
# 2. If param('ref'),param('start') and param('stop') are defined, or if param('q') is defined, then we
#    reset {ref,start,stop}
# 3. Otherwise, if {ref,start,stop} are defined, then
#    2a. interrogate param('span'). If span != (stop-start+1) then user has changed the zoom popup menu and we do a zoom.
#    2b. interrogate /left|right|zoom|nav|regionview|overview/, which define the various zoom and scroll buttons.
#        If any of them exist, then we do the appropriate coordinate adjustment
# 3. If we did NOT change the coordinates, then we look for param('name') and use that to set the coordinates
#    using a database search.
# 4. set {name} to "ref:start..stop"

sub update_coordinates {
  my $self  = shift;
  my $state = shift || $self->state;

  delete $self->{region}; # clear cached region
  my $position_updated;

  if (param('ref')) {
    $state->{ref}   = param('ref');
    $state->{view_start} = param('start') if defined param('start') && param('start') =~ /^[\d-]+/;
    $state->{view_stop}  = param('stop')  if defined param('stop')  && param('stop')  =~ /^[\d-]+/;
    $state->{view_stop}  = param('end')   if defined param('end')   && param('end')   =~ /^[\d-]+/;
    $position_updated++;
  }

  # quench uninit variable warning
  my $current_span = length($state->{view_stop}||'') ? ($state->{view_stop} - $state->{view_start} + 1) 
                                                : 0;
  my $new_span     = param('span');
  if ($new_span && $current_span != $new_span) {
    $self->zoom_to_span($state,$new_span);
    $position_updated++;
  }
  elsif (my ($scroll_data) = grep {/^(?:left|right) \S+/} param()) {
    $self->scroll($state,$scroll_data);
    $position_updated++;
  }
  elsif (my ($zoom_data)   = grep {/^zoom (?:out|in) \S+/} param()) {
    $self->zoom($state,$zoom_data);
    $position_updated++;
  }
  elsif (my $position_data = param('overview.x')) {
    $self->position_from_overview($state,$position_data);
    $position_updated++;
  }
  elsif ($position_data = param('regionview.x')) {
    $self->position_from_regionview($state,$position_data);
    $position_updated++;
  }

  if ($position_updated) { # clip and update param
      if (defined $state->{seg_min} && $state->{view_start} < $state->{seg_min}) {
	  my $delta = $state->{seg_min} - $state->{view_start};
	  $state->{view_start} += $delta;
	  $state->{view_stop}  += $delta;
      }

      if (defined $state->{seg_max} && $state->{view_stop}  > $state->{seg_max}) {
	  my $delta = $state->{view_stop} - $state->{seg_max};
	  $state->{view_start} -= $delta;
	  $state->{view_stop}  -= $delta;
      }

      # Take details multiplier into account
      $self->update_state_from_details_mult;

      # update our "name" state and the CGI parameter
      $state->{name} = $self->region_string;
      param(name => $state->{name});

      warn "name = $state->{name}" if DEBUG;
  }

  elsif (param('name') || param('q')) {
      $state->{backup_region} = 
	  [$state->{ref},$state->{start},$state->{stop},$state->{view_start},$state->{view_stop}] if $state->{ref};
      undef $state->{ref};  # no longer valid
      undef $state->{start};
      undef $state->{stop};
      undef $state->{view_start};
      undef $state->{view_stop};
      $state->{name}       = $state->{search_str} = param('name') || param('q');
      $state->{dbid}       = param('dbid'); # get rid of this
  }
}

sub asynchronous_update_overview_scale_bar {
    my $self = shift;
    my $seg  = $self->segment;

    my $renderer = $self->get_panel_renderer($seg);
    my ( $url, $height, $width ) = $renderer->render_scale_bar(
        section       => 'overview',
        whole_segment => $self->whole_segment,
        segment       => $seg,
        state         => $self->state
    );

    my $image_id = OVERVIEW_SCALE_LABEL."_image";

    return {
        url      => $url,
        height   => $height,
        width    => $width,
        image_id => $image_id,
    };
}

sub debug_visible {
    my $self  = shift;
    my $state = shift;
    warn "[$$] ",join ' ',map {
	$_ . '=>' . $state->{features}{$_}{visible}
    }
    @{$state->{tracks}};
}

sub asynchronous_update_region_scale_bar {
    my $self = shift;
    my $seg  = $self->segment;

    my $renderer = $self->get_panel_renderer($seg);
    my ( $url, $height, $width ) = $renderer->render_scale_bar(
        section       => 'region',
        region_segment => $self->region_segment,
        segment       => $seg,
        state         => $self->state
    );

    my $image_id = REGION_SCALE_LABEL."_image";

    return {
        url      => $url,
        height   => $height,
        width    => $width,
        image_id => $image_id,
    };
}

sub asynchronous_update_detail_scale_bar {
    my $self = shift;
    my $seg  = $self->segment;

    my $renderer = $self->get_panel_renderer($seg);
    my ( $url, $height, $width ) = $renderer->render_scale_bar(
        section => 'detail',
        segment => $seg,
        state   => $self->state
    );

    my $image_id = DETAIL_SCALE_LABEL . "_image";

    my ($scale_size, $scale_label) = $renderer->calculate_scale_size($seg->length/$self->details_mult, $width/$self->details_mult);

    return {
        url         => $url,
        height      => $height,
        width       => $width,
        view_width  => $self->get_image_width($self->state),
        image_id    => $image_id,
        scale_size  => $scale_size,
        scale_label => $scale_label,
    };
}

sub asynchronous_update_sections {
    my $self          = shift;
    my $section_names = shift;

    # avoid unecessary database inits
    #    $self->init_database();

    my $source        = $self->data_source;
    my $return_object = {};

    my %handle_section_name = map { $_ => 1 } @{ $section_names || [] };

    # Init Plugins if need be
    if (   $handle_section_name{'plugin_configure_div'}
        || $handle_section_name{'tracks_panel'}
	|| $handle_section_name{'plugin_form'})
    {
        $self->init_plugins();
    }

    # Page Title
    if ( $handle_section_name{'page_title'} ) {
	my $segment     = $self->thin_segment;  # avoids a db open
        my $dsn         = $self->data_source;
        my $description = $dsn->description;
        $return_object->{'page_title'} = $self->generate_title([$segment]);
    }

    # Span that shows the range
    if ( $handle_section_name{'span'} ) {
        my $container
	    = $self->slidertable();
        $return_object->{'span'} = $container;
    }

    # Unused Search Field
    if ( $handle_section_name{'search_form_objects'} ) {
        $return_object->{'search_form_objects'} 
	    = $self->render_search_form_objects();
    }

    if ($handle_section_name{'login_menu'}) {
	$return_object->{login_menu} = $self->login_manager->render_login();
    }

    # Plugin Configuration Form
    # A params is used to determine the plugin
    if ( $handle_section_name{'plugin_configure_div'} ) {
        my $plugin_base = param('plugin_base');
        if ($plugin_base) {
            my $plugin = $self->plugins->plugin($plugin_base);
            if ($plugin) {
                $return_object->{'plugin_configure_div'}
                    = $self->wrap_plugin_configuration( $plugin_base,
                    $plugin );
            }
            else {
                $return_object->{'plugin_configure_div'}
                    = $self->translate('NOT_RECOGNIZED_PLUGIN',$plugin_base)||'' . "\n";
            }
        }
        else {
            $return_object->{'plugin_configure_div'}
                = $self->translate('NO_PLUGIN_SPECIFIED')||'' . "\n";
        }
    }

    # Galaxy form
    if ( $handle_section_name{'galaxy_form'} ) {
	$return_object->{'galaxy_form'} = $self->galaxy_form($self->thin_segment);
    }

    # Plugin form
    if ( $handle_section_name{'plugin_form'} ) {
	$return_object->{'plugin_form'} = $self->plugin_form();
    }

    # Track Checkboxes
    if ( $handle_section_name{'tracks_panel'} ) {
        $return_object->{'tracks_panel'} = $self->render_track_table();
    }

    # New Custom Tracks Section
    if ( $handle_section_name{'custom_tracks'}) {
	$return_object->{'custom_tracks'} = $self->render_custom_track_listing();
    }
    
    # Community Tracks Section
    if ( $handle_section_name{'community_tracks'}) {
	$return_object->{'community_tracks'} = $self->render_community_track_listing(@_); #Passing on any search terms.
    }

    # Saved Snapshot Section
    if ( $handle_section_name{'snapshots_page'}) {
	$return_object->{'snapshots_page'} = $self->snapshot_manager->render_snapshots_listing($self);
    }

    # Handle Remaining and Undefined Sections
    foreach my $section_name ( keys %handle_section_name ) {
        next if ( defined $return_object->{$section_name} );
        $return_object->{$section_name} = 'Unknown element: ' . $section_name;
    }
    return $return_object;
}

# asynchronous_update_element has been DEPRECATED
# in favor of asynchronous_update_sections
sub asynchronous_update_element {
    my $self    = shift;
    my $element = shift;
    $self->init_database();
    my $source = $self->data_source;

    if ( $element eq 'page_title' ) {
        my $segment     = $self->segment;
        my $dsn         = $self->data_source;
        my $description = $dsn->description;
	my $divider     = $dsn->unit_divider;
        return $description . '<br>'
            . $self->translate(
            'SHOWING_FROM_TO',
            scalar $source->unit_label( $segment->length ),
            $segment->seq_id,
            $source->commas( $segment->start/$divider ),
            $source->commas( $segment->end/$divider )
            );
    }
    elsif ( $element eq 'span' ) {  # this is the popup menu that shows ranges
        my $container
            = $self->zoomBar( $self->segment, $self->whole_segment );
        $container =~ s/<\/?select.+//g;
        return $container;
    }
    elsif ( $element eq 'landmark_search_field' ) {
        return $self->state->{name};
    }
    elsif ( $element eq 'overview_panels' ) {
        return "<b>some day this will be the overview showing "
            . $self->state->{name} . "</b>";
    }
    elsif ( $element eq 'detail_panels' ) {
        $self->init_plugins();
	$self->init_remote_sources();
        return join ' ',
            $self->render_detailview_panels( $self->region->seg );
    }
    elsif ( $element eq 'plugin_configure_div' ) {
        $self->init_plugins();
        my $plugin_base = param('plugin_base');
        my $plugin      = $self->plugins->plugin($plugin_base)
            or return "$plugin_base is not a recognized plugin\n";

        return $self->wrap_plugin_configuration($plugin_base,$plugin);
    }
    elsif ( $element eq 'external_utility_div' ) {
        if ( my $file_name = param('edit_file') ) {
            $file_name = CGI::unescape($file_name);
            return $self->edit_uploaded_file($file_name);
        }
        elsif ( param('new_edit_file') ) {
            my $file_name = $self->uploaded_sources->new_file_name();
            return $self->edit_uploaded_file($file_name);
        }
    }

    # Track Checkboxes
    elsif ( $element eq 'tracks_panel' ) {
        $self->init_plugins();
        return $self->render_track_table();
    }
    # External Data Form
    elsif ( $element eq 'upload_tracks_panel' ) {
        return $self->render_external_table();
    }

    return 'Unknown element: ' . $element;
}

sub asynchronous_update_coordinates {
    my $self   = shift;
    my $action = shift;

    my $state  = $self->state;

    my $whole_segment_start = $state->{seg_min};
    my $whole_segment_stop  = $state->{seg_max};

    my $position_updated;
    if ($action =~ /left|right/) {
	$self->scroll($state,$action);
	$position_updated++;
    }
    if ($action =~ /zoom/) {
	$self->zoom($state,$action);
	$position_updated++;
    }
    if ($action =~ /set span/) {
	$self->zoom_to_span($state,$action);
	$position_updated++;
    }
    if ($action =~ /set segment/) {
	$self->move_segment($state,$action);
	$position_updated++;
    }
    if ($action =~ /reload segment/) {
	$position_updated++;
    }
    if ( $action =~ /flip (\S+)/ ) {
	if ( $action =~ /name/) {
	    $self->move_to_name($state, $action);
	    $position_updated++;
	}
	if ( $1 eq 'true' ) {
	    $state->{'flip'} = 1;
	}
	else {
	    $state->{'flip'} = 0;
	}
    }

    if ($position_updated) { # clip and update param
	if (defined $whole_segment_start && $state->{view_start} < $whole_segment_start) {
	    my $delta = $whole_segment_start - $state->{view_start};
	    $state->{view_start} += $delta;
	    $state->{view_stop}  += $delta;
	}

	if (defined $whole_segment_stop && $state->{view_stop}  > $whole_segment_stop) {
	    my $delta = $state->{view_stop} - $whole_segment_stop;
	    $state->{view_start} -= $delta;
	    $state->{view_stop}  -= $delta;

            if ($state->{view_start} < 0) {
                # Segment requested is larger than the whole segment
                $state->{view_start} = $whole_segment_start;
	        $state->{view_stop}  = $whole_segment_stop;
            }
	}

        # Take details multiplier into account
        $self->update_state_from_details_mult;

	unless (defined $state->{ref}) {
	    warn "Reverting coordinates to last known good region (user probably hit 'back' button).";
	    if ($state->{backup_region}) { # last known working region
		@{$state}{'ref','start','stop','view_start','view_stop'} = @{$state->{backup_region}};
	    } else {
		$state->{name} = param('name') || param('q') || url_param('name') || url_param('q'); # get the region somehow!!
		if (my $seg = $self->segment) {
		    $state->{ref}   = $seg->seq_id;
		    $state->{start} = $seg->start;
		    $state->{stop}  = $seg->stop;
		}
	    }
	}

	# update our "name" state and the CGI parameter
	$state->{name} = $self->region_string;
    }
    $position_updated;
}

sub update_state_from_details_mult {
    my $self = shift;
    my $state = $self->state;

    my $view_start = $state->{view_start} || 0;
    my $view_stop  = $state->{view_stop}  || 0;

    my $details_mult = $self->details_mult;

    my $length         = $view_stop - $view_start;
    my $length_to_load = int($length * $details_mult);

    my $start_to_load  = int($view_start - $length * ($details_mult - 1) / 2);

    if (defined $state->{seg_min} && $start_to_load < $state->{seg_min}) {
        $start_to_load = $state->{seg_min};
    }

    my $stop_to_load   = $start_to_load + $length_to_load;

    if (defined $state->{seg_max} && $stop_to_load > $state->{seg_max}) {
        my $delta = $stop_to_load - $state->{seg_max};
        $start_to_load -= $delta;
        $stop_to_load  -= $delta;
    }

    $state->{start} = $start_to_load;
    $state->{stop}  = $stop_to_load;
}

sub region_string {
    my $self    = shift;
    my $state   = $self->state;
    my $source  = $self->data_source;
    my $divider = $source->unit_divider || 1;
    $state->{view_start}  ||= 0;
    $state->{view_stop}   ||= 0;
    return "$state->{ref}:".
	$source->commas($state->{view_start}/$divider).
	'..'.
	$source->commas($state->{view_stop}/$divider);
}

sub zoom_to_span {
  my $self = shift;
  my ($state,$new_span) = @_;

  my ($span) = $new_span =~ /([\d+.-]+)/;

sub move_to_name {
  my $self = shift;
  my ( $state, $new_name ) = @_;

  if ( $new_name =~ /:(.*):([\d+.-]+)\.\.([\d+.-]+)/ ) {
    my $new_chr   = $1;
    my $new_start = $2;
    my $new_stop  = $3;

    $state->{ref} = $new_chr;
    $state->{view_start} = $new_start;
    $state->{view_stop}  = $new_stop;
    $self->background_track_render();
  }
}

  my $current_span = $state->{view_stop} - $state->{view_start} + 1;
  my $center	    = int(($current_span / 2)) + $state->{view_start};
  my $range	    = int(($span)/2);
  $state->{view_start}   = $center - $range;
  $state->{view_stop }   = $state->{view_start} + $span - 1;
}

sub move_segment {
  my $self = shift;
  my ( $state, $new_segment ) = @_;

  if ( $new_segment =~ /:([\d+.-]+)\.\.([\d+.-]+)/ ) {
    my $new_start = $1;
    my $new_stop  = $2;

    $state->{view_start} = $new_start;
    $state->{view_stop}  = $new_stop;
  }
}

sub scroll {
  my $self = shift;
  my $state       = shift;
  my $scroll_data = shift;

  my $flip        = $state->{flip} ? -1 : 1;

  $scroll_data    =~ s/\.[xy]$//; # get rid of imagemap button cruft
  my $scroll_distance = $self->unit_to_value($scroll_data);

  $state->{view_start} += $flip * $scroll_distance;
  $state->{view_stop}  += $flip * $scroll_distance;
}

sub zoom {
  my $self = shift;
  my $state     = shift;
  my $zoom_data = shift;

  $zoom_data    =~ s/\.[xy]$//; # get rid of imagemap button cruft
  my $zoom_distance = $self->unit_to_value($zoom_data);
  my $span          = $state->{view_stop} - $state->{view_start} + 1;
  my $center	    = int($span / 2) + $state->{view_start};
  my $range	    = int($span * (1-$zoom_distance)/2);
  $range            = 1 if $range < 1;

  my $newstart      = $center - $range;
  my $newstop       = $center + $range - 1;
  
  if ($newstart==$state->{view_start} && $newstop==$state->{view_stop}) {
      if ($zoom_distance < 0) {$newstart--;$newstop++};
      if ($zoom_distance > 0) {$newstart++;$newstop--};
  }
  if ($newstop-$newstart <=2) {$newstop++}  # don't go down to 2 bp level!

  $state->{view_start}   = $newstart;
  $state->{view_stop}    = $newstop;
}

sub position_from_overview {
  my $self = shift;
  my $state         = shift;
  my $position_data = shift;

  return unless defined $state->{seg_max} && defined $state->{seg_min};

  my $segment_length = $state->{seg_max} - $state->{seg_min} + 1;
  return unless $segment_length > 0;

  my @overview_tracks = grep {$state->{features}{$_}{visible}} 
    $self->data_source->overview_tracks;

  my ($padl,$padr)   = $self->overview_pad(\@overview_tracks);
  my $overview_width = $state->{width} * $self->overview_ratio;

  my $click_position = $state->{seg_min} + $segment_length * ($position_data-$padl)/$overview_width;
  my $span           = $state->{stop} - $state->{start} + 1;

  $state->{start}    = int($click_position - $span/2);
  $state->{stop}     = $state->{start} + $span - 1;
}

sub position_from_regionview {
  my $self = shift;
  my $state         = shift;
  my $position_data = shift;
  return unless defined $state->{seg_max} && defined $state->{seg_min};
  return unless $state->{region_size};

  my @regionview_tracks = grep {$state->{features}{$_}{visible}}
    $self->data_source->regionview_tracks;

  my ($padl,$padr) = $self->overview_pad(\@regionview_tracks) or return;
  my $regionview_width = ($state->{width} * $self->overview_ratio);

  my $click_position = $state->{region_size}  * ($position_data-$padl)/$regionview_width;
  my $span           = $state->{stop} - $state->{start} + 1;

  my ($regionview_start, $regionview_end) = $self->regionview_bounds();

  $state->{start} = int($click_position - $span/2 + $regionview_start);
  $state->{stop}  = $state->{start} + $span - 1;
}

sub update_region {
  my $self  = shift;
  my $state = shift || $self->state;

  if ($self->setting('region segment')) {
    $state->{region_size} = param('region_size') 
	if defined param('region_size');
    $state->{region_size} = $self->setting('region segment') 
	unless defined $state->{region_size};
  }
  else {
    delete $state->{region_size};
  }
}

sub update_section_visibility {
  my $self = shift;
  my $state = shift;

  for my $div (grep {/^div_visible_/} CGI::cookie()) {
    my ($section)   = $div =~ /^div_visible_(\w+)/ or next;
    my $visibility  = CGI::cookie($div);
    $state->{section_visible}{$section} = $visibility;
  }
}

sub update_galaxy_url {
    my $self  = shift;
    my $state = shift;
    if (my $url = param('GALAXY_URL')) {
	warn "[$$] setting galaxy" if DEBUG;
	$state->{GALAXY_URL} = $url;
    } elsif (param('clear_galaxy')) {
	warn "clearing galaxy" if DEBUG;
	delete $state->{GALAXY_URL};
    }
}

##################################################################3
#
# SHARED RENDERING CODE HERE
#
##################################################################3

# overview_ratio and overview_pad moved to RenderPanels.pm

sub set_language {
  my $self = shift;

  my $data_source = $self->data_source;

  my $lang             = Bio::Graphics::Browser2::I18n->new($data_source->globals->language_path);
  my $default_language = $data_source->setting('language') || 'POSIX';

  my $accept           = CGI::http('Accept-language') || '';
  my @languages        = $accept =~ /([a-z]{2}-?[a-z]*)/ig;
  push @languages,$default_language if $default_language;

  return unless @languages;
  $lang->language(@languages);
  $self->language($lang);
  Bio::Graphics::Browser2::Util->set_language($lang);
}

sub language {
  my $self = shift;
  my $d = $self->{lang};
  $self->{lang} = shift if @_;
  $d;
}

# Returns the language code, but only if we have a translate table for it.
sub language_code {
  my $self = shift;
  my $lang = $self->language;
  my $table= $lang->tr_table($lang->language);
  return unless %$table;
  return $lang->language;
}

##### language stuff
sub label2key {
  my $self  = shift;
  my $label = shift;
  my $source = $self->data_source;
  my $key;

  # make URL labels a bit nicer
  if ($label =~ /^ftp|^http/) {
    $key = $source->setting($label => 'key') || url_label($label);
  }
  elsif ($label =~ /^plugin/) {
      ($key = $label) =~ s/^plugin://;
  }

  my $presets = $self->get_external_presets || {};
  for my $l ($self->language->language) {
    $key     ||= $source->setting($label=>"key:$l");
  }
  $key     ||= $source->setting($label => 'key');
  $key     ||= $key if defined $key;
  $key     ||= $label;
  $key;
}

# convert Mb/Kb back into bp... or a ratio
sub unit_to_value {
  my $self = shift;
  my $string = shift;
  my $sign           = $string =~ /out|left/ ? '-' : '+';
  my ($value,$units) = $string =~ /([\d.]+)(\s*\S+)?/;
  return unless defined $value;
  $units ||= 'bp';
  $value /= 100   if $units eq '%';  # percentage;
  $value *= 1000  if $units =~ /kb/i;
  $value *= 1e6   if $units =~ /mb/i;
  $value *= 1e9   if $units =~ /gb/i;
  return "$sign$value";
}

sub get_zoomincrement {
  my $self = shift;
  my $zoom = $self->setting('fine zoom');
  return $zoom;
}

sub get_max_segment {
  my $self = shift;
  my $divisor   = $self->setting('unit_divider') || 1;
  my $max_seg   = $self->setting('max segment');
  if (!$max_seg) {
    return MAX_SEGMENT;
  } elsif ($divisor == 1 ) {
    return $max_seg
  } else {
    return $max_seg * $divisor;
  }
}


#############################################################################
#
# HANDLING SEGMENTS
#
#############################################################################
sub regionview_bounds {
  my $self  = shift;
  my $segment = $self->thin_region_segment;
  return ($segment->start,$segment->end);
}

# this version handles labels with embedded hyphens correctly
sub split_labels_correctly {
  my $self = shift;
  return map {split LABEL_SEPARATOR,$_} @_;
}

# this version does not handle labels with embedded "+" or "-"
# unless the hyphen is escaped with %01d
sub split_labels {
  my $self = shift;
  my @results;

  for (@_) {

      # pass URLs through unmodified
      if (/^(http|ftp|das)/) {
	  push @results,$_;
	  next;
      }
      push @results, split /[+-]/;
  }

  my $group_separator = GROUP_SEPARATOR;
  foreach (@results) {
      s/$group_separator/-/g;  # unescape hyphens
      s/$;/-/g;                # unescape hyphens -backward compatibility
  }

  @results;
}

# remove any tracks that the client still thinks are valid
# but which have been removed from the config file
sub remove_invalid_tracks {
    my $self = shift;
    my $state = shift;

    my %potential = map {$_=>1} $self->potential_tracks;
    my @defunct   = grep {!$potential{$_}} keys %{$state->{features}};
    delete $state->{features}{$_} foreach @defunct;
    $state->{tracks} = [grep {$potential{$_}} @{$state->{tracks}}];
}

sub set_tracks {
    my $self   = shift;
    my @labels = @_;
    my $state  = $self->state;

    my %potential = map {$_=>1} $self->potential_tracks;

    my @main;
    for my $label (@labels) {
	my ($main,$subtracks) = split '/',$label;
	$subtracks ||= '';
	my @subtracks         = shellwords($subtracks);
	foreach (@subtracks) {s/#.+$//}  # get rid of comments
	push @main,$main;
	$state->{subtracks}{$main} = \@subtracks if @subtracks;
    }

    my %seenit;

    $state->{tracks} = [grep {$potential{$_} && !$seenit{$_}++} @main];
    $self->load_plugin_annotators(\@main);
    $state->{features}{$_}{visible} = 0 foreach $self->data_source->labels;
    $state->{features}{$_}{visible} = 1 foreach @main;
}

sub load_plugin_annotators {
  my ($self,$visible_labels) = @_;

  my %label_visible = map { $_ => 1 } @{ $visible_labels || [] };
  my $state = $self->state;
  my $source = $self->data_source;
  my %default_plugin = map {$_=>1} map {s/^plugin:// && $_}
    grep {/^plugin:/} $source->default_labels;

  my %listed           = $state->{tracks} ? map {$_=>1} @{$state->{tracks}} : (); # are we already on the list?
  my %listed_in_source = map {$_=>1} $source->configured_types;                   # are we already on the list?

  for my $plugin ($self->plugins->plugins) {
    next unless $plugin->type eq 'annotator';
    my $name = $plugin->id;  # use the ID "RestrictionAnnotator" rather than the name "Restriction Annotator"
    $name = "plugin:$name";
    $source->add_type($name,{}) unless $listed_in_source{$name}++;
    $state->{features}{$name} ||= {visible=>$label_visible{$name}||0,options=>0,limit=>0};
  }
}

sub detail_tracks {
  my $self     = shift;
  my $external = $self->external_data;
  my %files_in_details = map {$_=>1} $self->featurefiles_in_section('detail','expand');
  my @tracks           = grep {$files_in_details{$_}
			       || !/:(overview|region)$/} $self->visible_tracks;
  return @tracks;
}

sub overview_tracks {
  my $self = shift;
  my @tracks = grep {/:overview$/} $self->visible_tracks;
  my @files_in_overview  = $self->featurefiles_in_section('overview');
  my %seen;
  return grep {!$seen{$_}++} (@tracks,@files_in_overview);
}

sub regionview_tracks {
  my $self = shift;
  my @tracks = grep {/:region$/}   $self->visible_tracks;
  my @files_in_region  = $self->featurefiles_in_section('region');
  my %seen;
  return grep {!$seen{$_}++}  (@tracks,@files_in_region);
  
}

# all tracks currently in our state; this MAY go out of date if the
# configuration file changes.
sub all_tracks {
    my $self   = shift;
    my $state  = $self->state;
    my $source = $self->data_source;
    return grep {$source->authorized($_)} @{$state->{tracks}};
}

# all potential tracks; this is guaranteed to be up to date with the
# configuration file.
sub potential_tracks {
    my $self   = shift;
    my $source  = $self->data_source;
    my %seenit;
    return grep {!$seenit{$_}++
	      && !/^_/} ($source->detail_tracks,
			 $source->overview_tracks,
			 $source->plugin_tracks,
			 $source->regionview_tracks,
                        );
}

sub visible_tracks {
    my $self  = shift;
    my $state = $self->state;
    my @tracks = grep {$state->{features}{$_}{visible} 
		 && !/^_/
           } $self->all_tracks;
    return @tracks;
}

sub featurefiles_in_section {
    my $self             = shift;
    my $desired_section  = shift;
    my $expand           = shift;

    my $external = $self->external_data;
    my $state    = $self->state;
    my %found;

    for my $label (keys %$external) {
	$state->{features}{$label}{visible}   or next;
	my $file     = $external->{$label}    or next;
	my %sections = map {$_=>1} $self->featurefile_sections($label);
	$label .= ":".lc $desired_section if $expand;
	$found{$label}++ if $sections{lc $desired_section};
    }
    return keys %found;
}

sub featurefile_sections {
    my $self  = shift;
    my $label = shift;
    my $ff    = $self->external_data->{$label} or return;
    return $self->_featurefile_sections($ff);
}

sub _featurefile_sections {
    my $self = shift;
    my $ff   = shift;
    return 'detail' if $ff->isa('Bio::Das::Segment');

    my %sections;

    # we prefer to read the labels from the feature file,
    # but some of the featurefile types don't support this.
    my @labels     = eval {$ff->labels};
    for my $label (@labels) {
	my $section = $1 if $label =~ /:(overview|region|details?)$/i;
	$section  ||= $ff->setting($label => 'section');
	$section  ||= 'detail';
	$section    =~ s/details/detail/; # foo!
	$sections{lc $_}++ for $section   =~ /(\w+)/g;
    }

    # probe for unconfigured types, which will go into detail section by default
    my @unconfigured  = eval {grep{!$ff->type2label($_)} $ff->types};
    $sections{detail}++ if @unconfigured;

    # last chance!
    $sections{detail}++ unless %sections;
    return keys %sections;
}

# given a set of track names append the section names
# to those that correspond to file uploads
sub expand_track_names {
    my $self     = shift;
    my @tracks   = @_;

    my $external = $self->external_data;
    my $source   = $self->data_source;
    my @results;

    for my $t (@tracks) {
	if ($source->code_setting($t=>'remote feature')) {
	    push @results,$t;
	}
	elsif ($external->{$t}) {
	    my @sections = $self->featurefile_sections($t);
	    @sections    = ('detail') unless @sections;
	    push @results,"$t:$_" foreach @sections;
	}
	else {
	    push @results,$t;
	}
    }

    return @results;
 }

# This turns track names into IDs for use at the client side.
# This is necessary because tracks from external files/URLs
# may generate more than one track
sub trackname_to_id {
    my $self = shift;
    my ($name,$section) = @_;
    $self->data_source->is_remotetrack($name) ? $name
   :$self->external_data->{$name}             ? "$name:$section"
   : $name;
}

################## get renderer for this segment #########
sub get_panel_renderer {
  my $self   = shift;
  my $seg    = shift || $self->segment;
  my $whole  = shift || $self->whole_segment;
  my $region = shift || $self->region_segment;

  return Bio::Graphics::Browser2::RenderPanels->new(-segment        => $seg,
						   -whole_segment  => $whole,
						   -region_segment => $region,
						   -source         => $self->data_source,
						   -settings       => $self->state,
						   -language       => $self->language,
						   -render         => $self,
						  );}

################## image rendering code #############

# render_detailview is now obsolete, but we retain it
# because it is handy for regression testing.
sub render_detailview {
    my $self = shift;
    my $seg  = shift or return;
    my @panels = $self->render_detailview_panels($seg);
    my $drag_script = $self->drag_script('detail_panels','track');
    local $^W = 0; # quash uninit variable warning
    return div($self->toggle('Details',
			     div({-id=>'detail_panels',-class=>'track'},
				 @panels
			     )
	       )
	).$drag_script;
}

sub render_detailview_panels {
    my $self = shift;
    my $seg  = shift;

    my @labels   = $self->detail_tracks;
    my $renderer = $self->get_panel_renderer($seg);
    my $panels   = $renderer->render_panels(
	{
	    labels            => \@labels,
	    external_features => $self->external_data,
	    section           => 'detail',
	}
	);
    
    return map {$panels->{$_}} @labels;
}

sub get_blank_panels {
    my $self        = shift;
    my $track_names = shift;
    my $section     = shift;

    my $settings = $self->state;

    my $html  = '';
    my $image_width = $self->get_image_width($settings);
    foreach my $track_name ( @{ $track_names || [] } ) {

	my $divname = $self->trackname_to_id($track_name,$section);

	warn "$track_name => $divname" if DEBUG;

        my $track_html = $self->render_grey_track(
            track_id         => $track_name,
            image_width      => $image_width,
            image_height     => EMPTY_IMAGE_HEIGHT,
            image_element_id => $track_name . "_image",
        );
        $track_html = $self->wrap_track_in_track_div(
	    track_name => $track_name,
            track_id   => $divname,
            track_html => $track_html,
        );
        $html .= $track_html;
    }
    return $html;

}

sub get_image_width {
    my $self = shift;
    my $state = shift;
    my $image_width = $state->{'width'} + $self->get_total_pad_width;
    return int($image_width);
}

sub get_detail_image_width {
    my $self = shift;
    my $state = shift;
    my $image_width = $state->{'width'} * $self->details_mult + $self->get_total_pad_width;
    return int($image_width);
}

sub get_total_pad_width {
    my $self = shift;
    my $source = $self->data_source;
    my $renderer  = $self->get_panel_renderer($self->thin_segment,
					      $self->thin_whole_segment,
					      $self->thin_region_segment,
					      );
    my $padl      = $source->global_setting('pad_left');
    my $padr      = $source->global_setting('pad_right');
    my $image_pad = $renderer->image_padding;
    $padl = $image_pad unless defined $padl;
    $padr = $image_pad unless defined $padr;
    return $padl + $padr;
}

sub set_details_multiplier {
    my $self = shift;
    my $mult = shift || 1;
    $self->{'.details_mult'} = $mult;
}

sub details_mult {
    my $self = shift;
    $self->{'.details_mult'} ||= $self->_details_mult;
    return $self->{'.details_mult'};
}

sub _details_mult {
    my $self = shift;
    my $state = $self->state;
    return $self->data_source->details_multiplier($state);
}

sub render_deferred {
    my $self        = shift;

    my %args = @_;

    my $labels      = $args{labels}          || [ $self->detail_tracks ];
    my $seg         = $args{segment}         || $self->thin_segment;
    my $section     = $args{section}         || 'detail';
    my $cache_extra = $args{cache_extra}     || $self->create_cache_extra();
    my $external    = $args{external_tracks} || $self->external_data;
    my $nocache     = $args{nocache};
    
    warn '(render_deferred(',join(',',@$labels),') for section ',$section,' nocache=',$nocache if DEBUG;

    my $renderer   = $self->get_panel_renderer($seg,
					       $self->thin_whole_segment,
					       $self->thin_region_segment
	);

    my $h_callback = $self->make_hilite_callback();
    my $requests = $renderer->request_panels(
        {   labels           => $labels,
            section          => $section,
            deferred         => 1,
            whole_segment    => $self->thin_whole_segment(),
	    external_features=> $external,
            hilite_callback  => $h_callback || undef,
            cache_extra      => $cache_extra,
	    nocache          => $nocache || 0,
	    remotes          => $self->remote_sources,
	    render           => $self,
            flip => ( $section eq 'detail' ) ? $self->state()->{'flip'} : 0,
        }
    );

    return $requests;
}

sub render_grey_track {
    my $self             = shift;
    my %args             = @_;
    my $image_width      = $args{'image_width'};
    my $image_height     = $args{'image_height'};
    my $image_element_id = $args{'image_element_id'};
    my $track_id         = $args{'track_id'};

    my $renderer = $self->get_panel_renderer($self->thin_segment,
					      $self->thin_whole_segment,
					      $self->thin_region_segment,
					     );
    my $url      = $renderer->source->globals->button_url() . "/grey.png";

    my $html = $renderer->wrap_rendered_track(
        label    => $track_id,
        area_map => [],
        width    => $image_width,
        height   => $image_height,
        url      => $url,
	section  => 'detail',
        status   => '',
    );

    return $html;
}

sub render_error_track {
    my $self = shift;
    my %args             = @_;
    my $image_width      = $args{'image_width'};
    my $image_height     = $args{'image_height'} * 3;
    my $image_element_id = $args{'image_element_id'};
    my $track_id         = $args{'track_id'};
    my $error_message    = $args{'error_message'};

    my $gd               = GD::Image->new($image_width,$image_height);
    my $black            = $gd->colorAllocate(0,0,0);
    my $white            = $gd->colorAllocate(255,255,255);
    my $pink             = $gd->colorAllocate(255,181,197);
    $gd->filledRectangle(0,0,$image_width,$image_height,$pink);
    my $font             = GD->gdMediumBoldFont;
    my ($swidth,$sheight) = ($font->width * length($error_message),$font->height);
    my $xoff              = ($image_width - $swidth)/2;
    my $yoff              = ($image_height - $sheight - 3);
    $gd->string($font,$xoff,$yoff,$error_message,$black);
    my ($url,$path) = $self->data_source->generate_image($gd);

    return $self->get_panel_renderer->wrap_rendered_track(
        label    => $track_id,
        area_map => [],
        width    => $image_width,
        height   => $image_height,
        url      => $url,
	section  => 'detail',
        status   => '',
    );
}

sub render_deferred_track {
    my $self             = shift;
    my %args             = @_;
    my $cache_key        = $args{'cache_key'};
    my $track_id         = $args{'track_id'};

    my $renderer = $self->get_panel_renderer($self->thin_segment,
					     $self->thin_whole_segment,
					     $self->thin_region_segment
	);

    my $base  = $renderer->get_cache_base();
    my $cache = Bio::Graphics::Browser2::CachedTrack->new(
        -cache_base => $base,
        -key        => $cache_key,
	-cache_time => ($self->state->{cache} 
	                 ? $self->data_source->cache_time 
	                 : 0),
    );
    my $status_html = "<!-- " . $cache->status . " -->";

    warn "render_deferred_track(): $track_id: status = $status_html" if DEBUG;

    my $result_html = '';
    if ( $cache->status eq 'AVAILABLE' ) {
        my $result   = $renderer->render_tracks( { $track_id => $cache } );
        $result_html = $result->{$track_id};
    }
    elsif ($cache->status eq 'ERROR') {
	warn "[$$] rendering error track: ",$cache->errstr; # if DEBUG;
        my $image_width = $track_id =~ /overview|region/ ? $self->get_image_width($self->state)
	                                                 : $self->get_detail_image_width($self->state);
        $result_html   .= $self->render_error_track(
						  track_id       => $track_id,
						  image_width      => $image_width,
						  image_height     => EMPTY_IMAGE_HEIGHT,
						  image_element_id => $track_id . "_image",
						  error_message    => 'Track rendering error: '.$cache->errstr)
     } else {
        my $image_width = $self->get_detail_image_width($self->state);
        $result_html .= $self->render_grey_track(
						 track_id         => $track_id,
						 image_width      => $image_width,
						 image_height     => EMPTY_IMAGE_HEIGHT,
						 image_element_id => $track_id . "_image",
						 );
    }
    $result_html .= '';   # to prevent uninit warning
    warn "[$$] $track_id=>",$cache->status if DEBUG;
    return $status_html . $result_html;
}


# returns the fragment we need to use the scriptaculous drag 'n drop code
sub drag_script {
  my $self       = shift;
  my $div_name   = shift;
  my $div_part = shift;

  return <<END;
  <script type="text/javascript">
 // <![CDATA[
   create_drag('$div_name','$div_part');
 // ]]>
 </script>
END
}

##################### utilities #####################

sub make_hilite_callback {
  my $self = shift;
  my $state = $self->state();
  my @hiliters = grep {$_->type eq 'highlighter'} $self->plugins()->plugins;
  return unless @hiliters or ($state->{h_feat} && %{$state->{h_feat}});
  return sub {
    my $feature = shift;
    my $color;

    # run through the set of hilite plugins and give each one
    # a chance to choose the highlighting for its feature
    foreach (@hiliters) {
      $color ||= $_->highlight($feature);
    }
    return $color if $color;
   
    # if we get here, we select the search term for highlighting
    my %names = map 
                 {lc $_=> 1} grep { defined $_ }
                  $feature->display_name,
                  eval{$feature->get_tag_values('Alias')};
    return unless %names;
    $color ||= $state->{h_feat}{$_} foreach keys %names;
    return $color;
  }
}

sub categorize_track {
  my $self  = shift;
  my $label = shift;

  my $user_labels = $self->get_usertrack_labels;

  return $self->translate('OVERVIEW') if $label =~ /:overview$/;
  return $self->translate('REGION')   if $label =~ /:region$/;
  return $self->translate('EXTERNAL') if $label =~ /^(http|ftp|file):/;
  return $self->translate('ANALYSIS') if $label =~ /^plugin:/;

  if ($user_labels->{$label}) {
      my $cat = $self->user_tracks->is_mine($user_labels->{$label}) 
	  ? $self->translate('UPLOADED_TRACKS_CATEGORY')
	  : $self->translate('SHARED_WITH_ME_CATEGORY');
      return "$cat:".$self->user_tracks->title($user_labels->{$label});
  }

  my $category;
  for my $l ($self->language->language) {
    $category      ||= $self->setting($label=>"category:$l");
  }
  $category        ||= $self->setting($label => 'category');
  $category        ||= '';  # prevent uninit variable warnings
  $category         =~ s/^["']//;  # get rid of leading quotes
  $category         =~ s/["']$//;  # get rid of trailing quotes
  return $category ||= $self->translate('GENERAL');
}

sub is_safari {
  return (CGI::user_agent||'') =~ /safari/i;
}

sub external_data {
    my $self    = shift;
    my $segment = $self->segment or return { };
    my $state   = $self->state;

    return $self->{feature_files} if exists $self->{feature_files};

    # $f will hold a feature file hash in which keys are human-readable names of
    # feature files and values are FeatureFile objects.
    my $f           = {};
    my $max_segment  = $self->get_max_segment;
    my $search       = $self->get_search_object;
    my $meta_segment = $search->segment($segment);
    my $too_big      =  $segment && ($self->get_panel_renderer($segment)->segment_length > $max_segment);
    if (!$too_big && $segment) {
	my $search       = $self->get_search_object;
	my $rel2abs      = $search->coordinate_mapper($segment,1);
	my $rel2abs_slow = $search->coordinate_mapper($segment,0);
	my $plugins      = $self->plugins;
	eval {
	    $_->annotate($meta_segment,$f,
			 $rel2abs,$rel2abs_slow,$max_segment,
			 $self->whole_segment,$self->region_segment);
	} foreach ($self->plugins,$self->remote_sources);
    }
    return $self->{feature_files} = $f;
}
#
# Supplement data source with user uploads
sub add_user_tracks {
    my $self        = shift;
    my ($data_source,$uuid) = @_;
    $data_source ||= $self->data_source;
    my $files   = $self->user_tracks;
    my $userdb  = $self->userdb;
    my $session = $self->session;

    return if $self->is_admin;  # admin user's tracks are already in main config file.
    $self->session->uploadsid;

    my $userdata    = $self->user_tracks;
    my @user_tracks = $userdata->tracks;

    for my $track (@user_tracks) {
	my $config_path = $userdata->track_conf($track);
	eval {$data_source->parse_user_file($config_path)};
    }

    return @user_tracks;
}

# Delete the segments so that they can be recreated with new parameters
sub delete_stored_segments {
    my $self = shift;

    delete $self->{region};
    delete $self->{region_segment};
    delete $self->{whole_segment};
}

###################### link generation ##############
sub annotation_help {
  return shift->globals->url_base."/annotation_help.html";
}

sub general_help {
  return shift->globals->url_base."/general_help.html";
}

sub join_selected_tracks {
    my $self = shift;
    my $state = $self->state;

    my @selected = $self->visible_tracks;
    for (@selected) { # escape hyphens
		if ((my $filter = $state->{features}{$_}{filter}{values})) {
			my @subtracks = grep {$filter->{$_}} keys %{$filter};
			$_ .= "/@subtracks";
		}
    }
    return $self->join_tracks(\@selected);
}

sub join_tracks {
    my $self = shift;
    my $tracks = shift;
    return join LABEL_SEPARATOR,@$tracks;
}

sub bookmark_link {
  my $self     = shift;
  my $settings = shift;

  my $q = new CGI('');
  my @keys = qw(start stop ref width version flip grid);
  foreach (@keys) {
    $q->param(-name=>$_,   -value=>$settings->{$_});
  }
  $q->param(-name=>'id',   -value=>$settings->{userid});  # slight inconsistency here
  $q->param(-name=>'l',    -value=>$self->join_selected_tracks);

  $q->param(-name=>'h_region',-value=>$settings->{h_region}) if $settings->{h_region};
  my @h_feat= map {"$_\@$settings->{h_feat}{$_}"} keys %{$settings->{h_feat}};
  $q->param(-name=>'h_feat',  -value=>\@h_feat) if @h_feat;
  return "?".$q->query_string();
}

sub gff_dump_link {
  my $self     = shift;
  my $fasta    = shift;

  my $state     = $self->state;
  my $upload_id = $self->session->uploadsid;
  my $segment   = $self->thin_segment or return '';

  my $q = new CGI('');
  if ($fasta) {
      $q->param(-name=>'gbgff',   -value=>'Fasta');
      $q->param(-name=>'m',       -value=>'application/x-fasta');
  } else {
       $q->param(-name=>'gbgff',   -value=>'Save');
       $q->param(-name=>'m',       -value=>'application/x-gff3');
  }

  # This will now be detemined and added on the client side
  #$q->param('q'=>$segment->seq_id.':'.$segment->start.'..'.$segment->end);

  # we probably need this ?
  $q->param(-name=>'id',      -value=>$upload_id);

  # handle external urls
  return "?".$q->query_string();
}

sub dna_dump_link {
  my $self     = shift;
  my $link     = $self->gff_dump_link('fastaonly');
  return $link;
}

sub galaxy_link {
    my $self = shift;
    my $settings   = shift || $self->state;

    my $galaxy_url = $settings->{GALAXY_URL} 
                     || $self->data_source->global_setting('galaxy outgoing');

    warn "[$$] galaxy_link = $galaxy_url" if DEBUG;
    return '' unless $galaxy_url;
    my $clear_it  = $self->galaxy_clear;
    my $submit_it = q(document.galaxyform.submit());							#}) - Syntax highlight fixing.
    return "$clear_it;$submit_it";
}

sub galaxy_clear {
    return q(new Ajax.Request(document.URL,{method:'post',postBody:'clear_galaxy=1'}));
}


sub image_link {
    my $self = shift;
    my $settings = shift;
    my $format   = shift;

    $format      = 'GD' unless $format && $format=~ /^(GD|GD::SVG|PDF)$/;

    my $source   = $self->data_source->name;
    my $id       = $self->session->id;
    my $flip     = $settings->{flip} || param('flip') || 0;
    my $keystyle = $settings->{ks};
    my $grid     = $settings->{grid} || 0;
    my ($base,$s) = $self->globals->gbrowse_base;
    my $url      = "$base/gbrowse_img/$s";
    my $tracks   = $settings->{tracks};
    my $width    = param('view_width') || $settings->{width};
    my $start    = param('view_start') || $settings->{view_start};
    my $stop     = param('view_stop')  || $settings->{view_stop};
    $stop++;
    my $name     = "$settings->{ref}:$start..$stop";
    my $selected = CGI::escape($self->join_selected_tracks);
    my $options  = join '+',map { join '+', CGI::escape($_),$settings->{features}{$_}{options}
                             } map {/\s/?"$_":$_}
    grep {
		$settings->{features}{$_}{options}
    } @$tracks;
    $id        ||= ''; # to prevent uninit variable warnings
    my $img_url  = "$url/?name=$name;l=$selected;width=$width;id=$id";
    $img_url    .= ";flip=$flip"         if $flip;
    $img_url    .= ";options=$options"   if $options;
    $img_url    .= ";format=$format"     if $format;
    $img_url    .= ";keystyle=$keystyle" if $keystyle;
    $img_url    .= ";grid=$grid";
    $self->add_hilites($settings,\$img_url);

    return $img_url
}

sub add_hilites {
    my $self     = shift;

    my $settings = shift;
    my $img_url  = shift;

  # add feature hilighting
    if ($settings->{h_feat} && ref $settings->{h_feat} eq 'HASH') {
		for my $h (keys %{$settings->{h_feat}}) {
			$$img_url .= ";h_feat=$h\@$settings->{h_feat}{$h}";
		}
    }
  # add region hilighting
    if ($settings->{h_region} && ref $settings->{h_region} eq 'ARRAY') {
		for my $h (@{$settings->{h_region}}) {
			$$img_url .= ";h_region=$h";
		}
    }
}

sub svg_link {
    my $self = shift;
    my $settings = shift;
    return "?help=svg_image;flip=".($settings->{flip}||0);
}

sub fcgi_request {
    my $self = shift;
    return $FCGI_REQUEST if defined $FCGI_REQUEST;

    unless (eval 'require FCGI;1') {
	return $FCGI_REQUEST = 0;
    }

    my $request  = FCGI::Request(\*STDIN,\*STDOUT,\*STDERR,\%ENV,0,FCGI::FAIL_ACCEPT_ON_INTR());
    $FCGI_REQUEST = ($request && $request->IsFastCGI ? $request : 0);

    $FCGI_RUNNING++ if $FCGI_REQUEST;
    return $FCGI_REQUEST;
}

sub fcgi_running { return $FCGI_RUNNING }

sub fork {
    my $self = shift;
    
    $self->prepare_modperl_for_fork();
    $self->prepare_fcgi_for_fork('starting');

    my $child = CORE::fork();
    print STDERR "forked $child" if DEBUG;
    die "Couldn't fork: $!" unless defined $child;

    if ($child) { # parent process
	$self->session->was_forked('parent') if ref $self;
	$self->prepare_fcgi_for_fork('parent');
    }

    else {
	$self->session->was_forked('child')  if ref $self;
	Bio::Graphics::Browser2::DataBase->clone_databases();
	Bio::Graphics::Browser2::Render->prepare_fcgi_for_fork('child');
	if (ref $self) {
	    $self->userdb->clone_database()      if $self->userdb;
	    $self->user_tracks->clone_database() if $self->user_tracks;
	}
    }

    return $child;
}

sub prepare_modperl_for_fork {
    my $self = shift;
    my $r    = modperl_request() or return;
    if ($ENV{MOD_PERL_API_VERSION} < 2) {
	eval {
	    require Apache::SubProcess;
	    $r->cleanup_for_exec() 
	}
    };
}

sub prepare_fcgi_for_fork {
    my $self  = shift;
    my $state = shift;
    my $req   = $self->fcgi_request() or return;
    if ($state eq 'starting') {
	$req->Detach();
    } elsif ($state eq 'parent') {
	$req->Attach();
    } elsif ($state eq 'child') {
	$req->LastCall();
	$FCGI_REQUEST = 0;
	undef *FCGI::DESTROY;
    }
}

# try to generate a chrom sizes file
sub chrom_sizes {
    my $self   = shift;
    my $source = $self->data_source;
    my $mtime  = $source->mtime;
    my $name   = $source->name;
    my $sizes  = File::Spec->catfile($self->globals->tmpdir('chrom_sizes'),"$name.sizes");
    if (-e $sizes && (stat(_))[9] >= $mtime) {
	return $sizes;
    }
    $self->generate_chrom_sizes($sizes) or return;
    return $sizes;
}

sub generate_chrom_sizes {
    my $self  = shift;
    my $sizes = shift;

    my $source = $self->data_source;

    open my $s,'>',$sizes or die "Can't open $sizes for writing: $!";

    # First, try to query the default database for the
    # seq_ids it knows about.
    my $db     = $source->open_database or return;

    # Bio::DB::SeqFeature has a seq_ids method.
    if (my @seqids = eval {$db->seq_ids}) {
	for (@seqids) {
	    my $segment = $db->segment($_) or die "Can't find chromosome $_ in default database";
	    print $s "$_\t",$segment->length,"\n";
	}
	close $s;
	return 1;
    }

    # Bio::DasI objects have an entry_points method
    if (my @segs = eval {$db->entry_points}) {
	for (@segs) {
	    print $s $_,"\t",$_->length,"\n";
	}
	close $s;
	return 1;
    }

    # Otherwise we search for databases with associated fasta files
    # or indexed fasta (fai) files.
    my ($fasta,$fastai);
    my @dbs    = $source->databases;
    for my $db (@dbs) {
	my ($dbid,$adaptor,%args) = $source->db2args($db);
	my $fasta = $args{-fasta} or next;
	if (-e "$fasta.fai") {
	    $fastai ||= "$fasta.fai";
	} elsif (-e $fasta && -w dirname($fasta)) {
	    $fasta  ||= $fasta;
	}
    }

    return unless $fasta or $fastai;

    if (!$fastai && eval "require Bio::DB::Sam;1") {
	Bio::DB::Sam::Fai->load($fasta);
	$fastai = "$fasta.fai";
    }

    if ($fastai) { # fai file -- copy to sizes
	open my $f,$fastai or die "Can't open $fasta: $!";
	while (<$f>) {
	    my ($seqid,$length) = split /\s+/;
	    print $s "$seqid\t$length\n";
	}
	close $f;
    }

    elsif (eval "require Bio::DB::Fasta; 1") {
	my $fa = Bio::DB::Fasta->new($fasta);
	my @ids = $fa->ids;
	for my $i (@ids) {
	    print $s $i,"\t",$fa->length($i),"\n";
	}
	undef $fa;
    }

    close $s;
    return 1;
}

# Change IDs (Old Uploads ID, New Uploads ID, Old UserID, New User ID) - Sometimes the user's session (as stored by the ID) is unretrievable - this changes the session to a new one so that a user's login is still valid.
sub change_ids {
    my $self = shift;
    my $old_uploadsid = shift;
    my $new_uploadsid = shift;
    my $old_userid = shift;
    my $new_userid = shift;
    my $usertracks = $self->user_tracks;
    $usertracks->change_ids($old_uploadsid, $new_uploadsid, $old_userid, $new_userid);
    my $userdb = $self->{userdb};
    $userdb->change_ids($old_uploadsid, $new_uploadsid, $old_userid, $new_userid);
}

# The following functions are implemented in HTML.pm (or any other Render subclass), are never called, and are here for debugging.
sub format_autocomplete {
    croak "implement in subclass";
}

sub render_actionmenu {
    my $self = shift;
    croak "implement in subclass";
}

sub render_tabbed_pages {
    my $self = shift;
    my ($main,$upload_share,$config) = @_;
    croak "implement in subclass";
}

sub render_select_track_link {
    croak "implement in subclass";
}

sub render_upload_share_section {
    croak "implement in subclass";
}

sub render_top    {
  my $self     = shift;
  my $title    = shift;
  croak "render_top() should not be called in parent class";
}

sub render_title   {
  my $self     = shift;
  my $title    = shift;
  croak "render_title() should not be called in parent class";
}

sub render_navbar {
  my $self = shift;
  my $seg  = shift;
  croak "render_navbar() should not be called in parent class";
}

sub clear_highlights { croak 'implement in subclass' }

sub render_toggle_track_table {
  my $self = shift;
  croak "render_toggle_track_table() should not be called in parent class";
}

sub render_track_table {
  my $self = shift;
  croak "render_track_table() should not be called in parent class";
}

sub render_instructions {
  my $self  = shift;
  my $title = shift;
  croak "render_instructions() should not be called in parent class";
}
sub render_multiple_choices {
  my $self = shift;
  croak "render_multiple_choices() should not be called in parent class";
}

sub render_global_config {
  my $self = shift;
  croak "render_global_config() should not be called in parent class";
}

sub render_toggle_external_table {
  my $self = shift;
  croak "render_toggle_external_table() should not be called in parent class";
}

sub render_toggle_userdata_table {
  my $self = shift;
  croak "render_toggle_userdata_table() should not be called in parent class";
}

sub render_bottom {
  my $self = shift;
  my $features = shift;
  croak "render_bottom() should not be called in parent class";
}

sub html_frag {
  my $self = shift;
  croak "html_frag() should not be called in parent class";
}

sub fatal_error {
  my $self = shift;
  my @msg  = @_;
  croak 'Please call fatal_error() for a subclass of Bio::Graphics::Browser2::Render';
}

sub zoomBar {
    my $self = shift;
    croak 'Please define zoomBar() in a subclass of Bio::Graphics::Browser2::Render';
}

sub track_config {
  my $self     = shift;
  my $track_name    = shift;
  croak "track_config() should not be called in parent class";
}

sub select_subtracks {
  my $self       = shift;
  my $track_name = shift;
  croak "select_subtracks() should not be called in parent class";
}

sub share_track {
  my $self     = shift;
  my $track_name    = shift;
  croak "share_track() should not be called in parent class";
}

sub render_ruler_div {
  my $self = shift;
  croak "render_ruler_div() should not be called in parent class";
}

########## note: "sub tr()" makes emacs' syntax coloring croak, so place this function at end
sub translate {
	my $self = shift;
	my $lang = $self->language or return @_;
	$lang->translate(@_);
}

sub login_manager {
    my $self = shift;
    return $self->{login_manager} if exists $self->{login_manager};
    eval "require Bio::Graphics::Browser2::Render::Login" unless
	Bio::Graphics::Browser2::Render::Login->can('new');
    return $self->{login_manager} = Bio::Graphics::Browser2::Render::Login->new($self);
}

sub snapshot_manager {
    my $self = shift;
    return $self->{snapshot_manager} ||= Bio::Graphics::Browser2::Render::SnapshotManager->new($self);
}

sub feature_summary_message {
    my $self = shift;
    my ($event_type,$label) = @_;
    my $sticky = $event_type eq 'mousedown' || 0;
    my $message= $self->data_source->setting($label=>'key'). ' '.lc($self->tr('FEATURE_SUMMARY'));
    return "GBubble.showTooltip(event,'$message',$sticky)";
}

sub feature_interaction {
    my $self = shift;
    my ($event_type,$label,$feature) = @_;
    my $source    = $self->data_source;
    my $settings  = $self->state;
    my $tips      = $source->global_setting('balloon tips') && $settings->{'show_tooltips'};
    my $renderer  = $self->get_panel_renderer($self->segment);

    if ($tips) {
	my $sticky  = $source->setting($label,'balloon sticky');
	my $height  = $source->setting($label,'balloon height') || 300;

	my $stick   = defined $sticky ? $sticky : $event_type eq 'mousedown';
	$stick     ||= 0;
	my ($balloon_style,$balloon_action) 
	    = $renderer->balloon_tip_setting($event_type eq 'mousedown' ? 'balloon click' : 'balloon hover',$label,$feature,undef,undef);
	$balloon_action ||= $renderer->make_title($feature,undef,$label,undef) 
	    if $source->global_setting('titles are balloons') && $event_type eq 'mouseover';
	$balloon_style  ||= 'GBubble';
	if ($balloon_action) {
	    my $action = $balloon_action =~ /^(http|ftp):/
		? "$balloon_style.showTooltip(event,'<iframe width='+$balloon_style.maxWidth+' height=$height " .
		"frameborder=0 src=$balloon_action></iframe>',$stick,$balloon_style.maxWidth)"
		: "$balloon_style.showTooltip(event,'$balloon_action',$stick)";
	    return ('text/plain',$action)
	}
    }

    my $link   = $renderer->make_link($feature,undef,$label,undef);
    my $target = $renderer->make_link_target($feature,undef,$label,undef);
    return ('text/plain',$target ? "window.open('$link','$target')" : "document.location='$link'") if $link;
    return;
}
sub tr {
	my $self = shift;
	my $lang = $self->language or return @_;
	$lang->translate(@_);
}

1;

